7 Mar 2026
Fan-In Workflows in APS Automation: Variable Arguments and the WorkItem Combine API
One of the quieter but genuinely useful features in APS Automation API is Activity Variable Arguments (varargs). Combined with the WorkItem Combine API and DAS Intermediate Storage, it unlocks a clean fan-in workflow pattern — run parallel workitems, collect their outputs, and feed everything into a single combinator step. No custom orchestration glue required.
This post walks through a concrete example: plotting two AutoCAD drawings to PDF in parallel, then merging them into one final PDF using a vararg activity.
What Are Variable Arguments?
Normally, an activity declares a fixed set of named parameters. But what if the number of inputs isn't known at design time? That's the vararg problem.
Design Automation solves this with a reserved parameter name: "...". Any activity that includes "..." in its parameter list becomes a vararg activity. At workitem time, you can supply any number of arguments under any names — Design Automation maps them all to the vararg slot automatically.
Defining a VarArg Activity
Here's the mergepdf activity definition. Notice the "..." parameter alongside the fixed "final" output:
var activity = new Activity()
{
Appbundles = [ myApp ],
CommandLine =
[
$"\"$(appbundles[{PackageName}].path)\\MergePDF.bundle\\Contents\\MergePDF.exe\""
],
Engine = TargetEngine,
Parameters = new Dictionary<string, Parameter>()
{
{ "...", new Parameter() }, // vararg slot — accepts any number of inputs
{ "final", new Parameter() { Verb = Verb.Put, LocalName = "final.pdf" } }
},
Id = ActivityName
};
The "..." parameter acts as a catch-all. When a workitem runs against this activity, any argument name that doesn't match a declared parameter goes into the vararg slot. The engine downloads all of them into the working directory before the tool executes.
The MergePDF App Bundle
The app bundle itself is straightforward — a console app that scans the working directory for all *.pdf files and merges them using PDFsharp:
static void Main(string[] args)
{
var pdfFiles = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.pdf");
using var pdfDocument = new PdfDocument();
foreach (string pdfFile in pdfFiles)
{
using var fs = File.OpenRead(pdfFile);
var input = PdfReader.Open(fs, PdfDocumentOpenMode.Import);
pdfDocument.Version = input.Version;
foreach (PdfPage page in input.Pages)
pdfDocument.AddPage(page);
}
pdfDocument.Save("final.pdf");
Console.WriteLine("PDFs merged successfully!");
}
The app doesn't need to know how many PDFs it will receive — it just scans the directory. The vararg mechanism ensures they're all there.
Note: Use PDFsharp
6.1.0-preview-3or higher. Earlier versions throw exceptions when reading PDFs produced byAutoCAD.PlotToPDF+prod.
The Full Pipeline: Fan-In with WorkItem Combine
Here's where it comes together. The workflow has three steps:
DWG 1 -> PlotToPDF -> das://intermediate/first.pdf ---|
├-> MergePDF -> final.pdf
DWG 2 -> PlotToPDF -> das://intermediate/second.pdf --|
Step 1 — Part WorkItems (Plot DWG to PDF)
Each part workitem runs AutoCAD.PlotToPDF+prod and writes its output to DAS Intermediate Storage using the das://intermediate/ scheme. This avoids uploading the PDF to OSS only to download it again in the next step.
var partWorkItem = new WorkItem()
{
ActivityId = "AutoCAD.PlotToPDF+prod",
Arguments = new Dictionary<string, IArgument>()
{
{
"HostDwg", new XrefTreeArgument()
{
Url = objectId, // OSS object ID of the DWG
Verb = Verb.Get,
Headers = new Dictionary<string, string>()
{
{ "Authorization", bearerToken }
}
}
},
{
"Result", new XrefTreeArgument()
{
Verb = Verb.Put,
Url = $"das://intermediate/{inputKey}" // e.g. "first.pdf"
}
}
}
};
Create one of these for each DWG file. The intermediate URL is the handoff point between parts and combinator.
Step 2 — Combinator WorkItem (Merge PDFs)
The combinator runs the vararg mergepdf activity. The variable arguments ("first" and "second") don't need to match anything declared in the activity — they map into the "..." slot. Their LocalName values control how they appear on disk in the working directory.
var combinatorWorkItem = new WorkItem()
{
ActivityId = "xrefgetapp.mergepdf+prod",
Arguments = new Dictionary<string, IArgument>()
{
{
"first", new XrefTreeArgument() // vararg input 1
{
Verb = Verb.Get,
Url = $"das://intermediate/first.pdf",
LocalName = "first.pdf"
}
},
{
"second", new XrefTreeArgument() // vararg input 2
{
Verb = Verb.Get,
Url = $"das://intermediate/second.pdf",
LocalName = "second.pdf"
}
},
{
"final", new XrefTreeArgument() // fixed output parameter
{
Verb = Verb.Put,
Url = finalOssObjectId,
LocalName = "final.pdf",
Headers = new Dictionary<string, string>()
{
{ "Authorization", bearerToken }
}
}
}
},
LimitProcessingTimeSec = 900
};
Step 3 — Submit via /workitems/combine
The combine endpoint takes the part workitems and the combinator as a single payload. Design Automation guarantees the combinator won't start until all parts have completed successfully.
var payload = new JObject
{
["parts"] = JArray.FromObject(partsWorkItems),
["combinator"] = JObject.FromObject(combinatorWorkItem)
};
var request = new HttpRequestMessage(HttpMethod.Post, "/v3/workitems/combine")
{
Content = new StringContent(payload.ToString(), Encoding.UTF8, "application/json")
};
The response contains status objects for each part and for the combinator, which you can poll independently:
var (partsStatus, combinatorStatus) = await CreateWorkItemsCombineAsync(
partsWorkItems, combinatorWorkItem, headers);
// Poll parts in parallel
var partsTasks = partsStatus.Select(s => PollUntilDoneAsync(s)).ToList();
var combinatorTask = PollUntilDoneAsync(combinatorStatus);
await Task.WhenAll(Task.WhenAll(partsTasks), combinatorTask);
Key Concepts Recap
| Concept | How It Works |
|---|---|
Vararg Parameter ("...") |
Declared in the activity. Any workitem argument without a matching named parameter is routed here. |
| Variable Argument Names | In the workitem, use any names you like ("first", "second", "page3", etc.) — they all land in the vararg slot. |
| DAS Intermediate Storage | das://intermediate/<key> — fast in-platform handoff between workitems. No OSS round-trip needed. |
| WorkItem Combine API | POST /v3/workitems/combine — submits parts + combinator together. Combinator starts only after all parts succeed. |
| Auto-wiring | When using /combine, the outputs of part workitems with das://intermediate/ URLs are automatically available to the combinator's vararg inputs. |
When to Use This Pattern
The PDF merge example in this post is just one instance of a broader fan-in workflow pattern. Anytime you need to run N parallel workitems and collect their outputs into a single step, varargs + combine is the right tool — regardless of file type or operation:
- Batch processing: process N files in parallel, then aggregate results (e.g. extract data from multiple DWGs, combine into a single report)
- Report generation: plot multiple sheets to PDF, merge into one document
- Multi-file exports: convert several design files to a target format, then package the outputs into a single archive or deliverable
- Multi-model analysis: run analysis workitems against separate models, then consolidate findings in a combinator step
- Thumbnail generation: render previews of N assets in parallel, then stitch or index them together
The key insight is that the vararg slot suits indeterministic workflows — the number of inputs or outputs doesn't need to be known at design time. It's just a set of files dropped into or out of the working directory, determined by the verb. Your combinator tool decides what to do with them.
PDF merging happens to be a clear demo, but the orchestration machinery is the same for any fan-in scenario.
The vararg + combine pattern keeps your orchestration inside Design Automation rather than in your application logic. You submit one request, get back one combinator status to poll, and let the platform handle the fan-in.