Skip to content
Expedify
6 min

Workflow Call

Call one workflow from another, so a piece of logic lives in exactly one place. Declared inputs, binding values to them, loop mode, and the recursion guard.

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.

the logic that now lives in one place
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 →

Calling once. The loop fields are below.

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.

Loop mode: the callee runs once per item, and the results come back collected.

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.

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_call in 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

  1. Build a small workflow that takes one input and returns one value, and declare that input so it has a signature.
  2. Call it from another workflow, binding the input explicitly.
  3. Read the result downstream from {{workflowcall_1.output}}.
  4. 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.

Related lessons