Module · Logic
Workflow Call
Lesson 7 of 11 · 6 min
Once you have a few workflows, the same fragment starts showing up in all of them. The way you score a lead. The way you decide whether a deal is big. The three nodes that format an address properly. Copy it four times and you have four places to fix when the rule changes — and you will fix three.
Workflow Call runs another workflow from inside this one and waits for its answer. The shared logic lives once, and everything else calls it.
The callee is an ordinary workflow with a signature
A workflow that gets called is not a special kind of object. It is a normal workflow that happens to declare what it needs — a name, a type and whether it is required, per input. That declaration is what the caller binds to, and it is what makes the call readable instead of a bag of variables.
The callee: Score a lead
Two inputs in, a score out. Nothing about it is call-specific.
def process(inputs, context):
status = str(inputs.get("lead_status") or "").lower()
source = str(inputs.get("source") or "organic").lower()
score = {"hot": 80, "warm": 50, "cold": 20}.get(status, 10)
if source == "referral":
score += 15
return {"score": min(score, 100), "band": "A" if score >= 80 else "B" if score >= 50 else "C"}Because it is an ordinary workflow, you can open it, run it and debug it on its own — which is the practical reason to factor logic out rather than a theoretical one.
The caller binds values to those inputs
The caller
One node stands in for the whole scoring workflow.
Scroll for all 3 steps →
| Field | What it holds |
|---|---|
workflow_id | ID of the workflow to call |
input_mapping | Binding rows for the target workflow's declared inputs, keyed by input name. Each value is a static literal, a dynamic {{ref}} from the calling run, or {{ai}} / {{ai: hint}} (the calling agent fills it, in tool mode). Empty rows fall through to the target input's own default. |
output_mapping | Output field mapping from target workflow |
pass_all_inputs | Pass all inputs to target workflow (vs just specified mappings) Defaults to false. |
max_recursion_depth | Maximum recursion depth to prevent infinite loops Defaults to 10. |
workflow_id
- What it holds
- ID of the workflow to call
input_mapping
- What it holds
- Binding rows for the target workflow's declared inputs, keyed by input name. Each value is a static literal, a dynamic {{ref}} from the calling run, or {{ai}} / {{ai: hint}} (the calling agent fills it, in tool mode). Empty rows fall through to the target input's own default.
output_mapping
- What it holds
- Output field mapping from target workflow
pass_all_inputs
- What it holds
- Pass all inputs to target workflow (vs just specified mappings) Defaults to
false.
max_recursion_depth
- What it holds
- Maximum recursion depth to prevent infinite loops Defaults to
10.
What comes back arrives at {{workflowcall_1.output}} — so the caller above reads {{workflowcall_1.output.score}}, the key the callee returned.
Bind explicitly rather than turning on pass_all_inputs. Passing everything is quicker to set up and makes the callee depend on whatever its callers happen to have lying around — which is exactly the coupling you factored the workflow out to avoid. Explicit bindings are also the only way to see, from the caller, what the call actually needs.
Loop mode
The same node can call a workflow once per item in an array. That is why the backend still calls this node “Workflow Loop”, even though the canvas labels it Workflow Call.
| Field | What it holds |
|---|---|
loop_mode | Enable loop mode - execute workflow once per array item Defaults to false. |
loop_array_source | Array source for looping (e.g., {{customfunction_1.output}}) |
loop_item_input_field | Field name to pass current item to sub-workflow Defaults to current_item. |
loop_mode
- What it holds
- Enable loop mode - execute workflow once per array item Defaults to
false.
loop_array_source
- What it holds
- Array source for looping (e.g., {{customfunction_1.output}})
loop_item_input_field
- What it holds
- Field name to pass current item to sub-workflow Defaults to
current_item.
| You have | Use |
|---|---|
| A list, and the per-item work is a few nodes | loop — keep it inline where you can see it |
| A list, and the per-item work is a whole workflow you already have | workflow_call in loop mode |
A list, and the per-item work is a few nodes
- Use
loop— keep it inline where you can see it
A list, and the per-item work is a whole workflow you already have
- Use
workflow_callin loop mode
Results arrive as {{workflowcall_1.loop_results}}, with iterations, successful and failed beside them — so a batch that half-worked is visible rather than silent.
The recursion guard
A workflow can call a workflow that calls the first one. max_recursion_depth is the ceiling that stops that being infinite; it defaults to 10.
Treat the guard as a backstop, not a design. If a call chain is genuinely ten deep, the problem is the chain. Two levels — a workflow calling shared pieces — is almost always the right shape, and it is the one somebody else can follow.
When to factor a workflow out
- The same steps appear in three places. The classic reason, and the safest.
- The logic is a decision you want to change centrally — scoring, thresholds, routing rules.
- A piece is worth testing on its own. A callee can be run directly with sample inputs; an inline fragment cannot.
And the argument against, which matters just as much: every call is a jump for the reader. A workflow made of six calls tells you nothing about what it does. Factor out the pieces that are genuinely shared, and leave the rest where it can be seen.
Try it
- Build a small workflow that takes one input and returns one value, and declare that input so it has a signature.
- Call it from another workflow, binding the input explicitly.
- Read the result downstream from
{{workflowcall_1.output}}. - Now change the callee's logic and run the caller again — without touching the caller. That is the whole point of the node.
Next: Set Variable — the first lesson of the Data module, and the simplest way to hold a value for later.

