Composable Workflows
Martha workflows support conditional routing, LLM-based classification, sub-workflows, and parallel execution. This lets you build complex multi-step processes from simple building blocks.
This guide covers:
- Workflow designers — how to use choice, LLM router, sub-workflow, and parallel nodes
- Developers — how the executor processes these nodes and what to expect
Node Types
Choice Node (Conditional Routing)
A choice node evaluates a variable against a list of conditions and routes to the first matching branch.
{
"id": "route_by_priority",
"type": "CHOICE",
"config": {
"eval_variable": "{{inputs.priority}}",
"conditions": [
{ "operator": "equals", "value": "urgent" },
{ "operator": "equals", "value": "normal" }
]
}
}The executor evaluates conditions top-to-bottom. The first match routes to branch-0, the second to branch-1, etc. If nothing matches, the default branch is taken.
Edges
Connect choice outputs to downstream nodes using source_handle:
[
{ "from": "route_by_priority", "to": "urgent_handler", "source_handle": "branch-0" },
{ "from": "route_by_priority", "to": "normal_handler", "source_handle": "branch-1" },
{ "from": "route_by_priority", "to": "fallback_handler", "source_handle": "default" }
]Operators
| Operator | Description | Example |
|---|---|---|
equals | Exact string match | "urgent" matches "urgent" |
not_equals | Inverse of equals | |
contains | Substring check | "error" matches "connection error" |
not_contains | Inverse of contains | |
greater_than | Numeric comparison | "100" > "50" |
less_than | Numeric comparison | |
matches_regex | Regex pattern match | "^ERR-\\d+$" matches "ERR-404" |
is_empty | None, "", [], {} | |
is_not_empty | Inverse of is_empty |
Template Values
Both eval_variable and condition value support {{...}} templates:
{{inputs.field}}— user inputs passed to the workflow{{inputs.nested.path}}— nested input fields
!!! warning "Condition values cannot reference {{nodes.*}}" For security, condition values are restricted to {{inputs.*}} templates. Referencing other node outputs in conditions is not allowed.
LLM Router (AI Classification)
An LLM router uses an AI model to classify text input and route to the matching branch.
{
"id": "classify_intent",
"type": "CHOICE_LLM",
"config": {
"eval_variable": "{{inputs.message}}",
"llm_config": {
"model": "anthropic/claude-sonnet-4-5-20250929",
"temperature": 0
},
"conditions": [
{ "operator": "equals", "value": "billing" },
{ "operator": "equals", "value": "support" },
{ "operator": "equals", "value": "sales" }
]
}
}The LLM receives a classification prompt with the configured categories and input text. Its response is matched against conditions using the same operators as choice nodes.
LLM Config
| Field | Default | Description |
|---|---|---|
model | gpt-4o-mini | Any model supported by LiteLLM |
temperature | 0 | Lower = more deterministic classification |
prompt | (built-in) | Custom classification prompt. Use {input} and {categories} placeholders. |
If the LLM returns an unrecognized category, the default branch is taken. If the LLM call fails, the default branch is also taken (with an error dict).
Sub-Workflow (Composition)
A sub-workflow node executes another workflow as a child, with input/output mapping between parent and child.
{
"id": "run_validation",
"type": "SUB_WORKFLOW",
"config": {
"workflow_name": "validation_workflow",
"input_mapping": {
"document_id": "{{inputs.doc_id}}",
"strict_mode": true
},
"output_mapping": {
"validation_result": "nodes.validator.output"
},
"timeout_seconds": 120,
"on_error": "abort"
}
}Config Fields
| Field | Required | Default | Description |
|---|---|---|---|
workflow_name | Yes | — | Name of the child workflow to execute |
input_mapping | No | {} | Map parent context values to child inputs. Supports {{...}} templates. |
output_mapping | No | {} | Map child result paths back to parent. Uses dot-notation (e.g., nodes.step.output). |
timeout_seconds | No | 300 | Maximum execution time for the child workflow |
on_error | No | abort | abort = propagate error, fallback = use fallback_value |
fallback_value | No | {} | Value returned if on_error is fallback and the child fails |
Cycle Detection
Martha prevents recursive loops. If workflow A calls workflow B which calls workflow A, the cycle is detected at runtime and returns an error:
Recursive sub-workflow detected: wf_b -> wf_a -> wf_bMaximum nesting depth is 3 levels. Deeper nesting returns an error immediately.
Parallel Execution
A parallel node executes multiple branches concurrently and merges results.
Edge-Based Branches (Recommended)
Define parallel branches through edges with source_handle: parallel-*:
[
{ "from": "parallel_start", "to": "branch_a", "source_handle": "parallel-0" },
{ "from": "parallel_start", "to": "branch_b", "source_handle": "parallel-1" }
]Branches discovered from edges run concurrently and merge into a single dict keyed by branch node ID.
!!! note "Branch limit" A maximum of 20 parallel branches are allowed per node. This prevents resource exhaustion from malformed workflow definitions.
Combining Node Types
These node types compose naturally:
- Choice -> Parallel: Route to different parallel execution groups based on input
- Sub-workflow with Choice: A child workflow can contain its own choice routing
- LLM Router -> Sub-workflow: Classify intent, then run the appropriate sub-workflow
- Parallel -> Choice: Run branches in parallel, then route based on merged results
Diamond Convergence
Multiple branches can converge back to a single node:
Start -> Choice -> Branch A -> End
-> Branch B -> EndBoth branches connect to End, but only the taken branch executes. The end node sees results from whichever branch ran.
Related
- Iteration Nodes — For-each, filter, and reduce for array processing within workflows