Sub-workflows & contract
The startWorkflow activity starts another workflow. The calling workflow is the parent, the started workflow the child. The child gets its own dependency-injection scope.
Mode
waitForCompletion: true(default) → the parent waits for the child. The child'sreturnDatais mirrored into the parent asparam.*.waitForCompletion: false→ fire and forget. The parent receivesparam.workflowId/param.workflowName/param.executionId/param.waited.
Maximum call depth: 10. forEach shares the ISubWorkflowGate with startWorkflow.
Contract derivation
GET /{id}/contract returns a workflow's interface:
- Inputs from
manualTrigger.parameters. - Outputs from the
returnData.datakeys + system outputs (__executionId,__status,__workflowId,__workflowName).
Callers without Edit permission on the workflow's folder see *** instead of any non-null input default. The mask is display only; a parameter left unset still gets the real default at run time.
By-name lookup: an exact-case match wins, otherwise case-insensitive — ambiguous names (names are not unique) return 409 rather than a silent random hit. The engine (startWorkflow/forEach) resolves identically, so the designer never shows a contract the runtime cannot find.
Subtleties
HasManualTrigger=falsedoes not mean "cannot be called" —startWorkflowcan call any enabled workflow. It only means there is no declared input contract; the UI falls back to a free-form parameter table.- Several
returnDatanodes:HasMultipleReturnDataNodes=true. Only one wins per run (last write wins over the whole JSON, not per key). Outputs are "may be available", not guaranteed — the UI shows a warning. - Several
manualTriggernodes: parameters are deduplicated by name. Iftype/defaultdiverge, the first declaration wins andHasConflict=true— a UI warning, not a hard failure.Requiredis aggregated with OR. - Reserved output keys (
__executionId,__status,__workflowId,__workflowName) are silently filtered out of a user'sreturnData.dataand injected separately by the engine. - Disabled nodes (
manualTrigger/returnDatawithdata.disabled=true) are ignored — matching the engine's skip behaviour.