Skip to content

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.

json
{
  "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:

json
[
  { "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

OperatorDescriptionExample
equalsExact string match"urgent" matches "urgent"
not_equalsInverse of equals
containsSubstring check"error" matches "connection error"
not_containsInverse of contains
greater_thanNumeric comparison"100" > "50"
less_thanNumeric comparison
matches_regexRegex pattern match"^ERR-\\d+$" matches "ERR-404"
is_emptyNone, "", [], {}
is_not_emptyInverse 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.

json
{
  "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

FieldDefaultDescription
modelgpt-4o-miniAny model supported by LiteLLM
temperature0Lower = 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.

json
{
  "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

FieldRequiredDefaultDescription
workflow_nameYesName of the child workflow to execute
input_mappingNo{}Map parent context values to child inputs. Supports {{...}} templates.
output_mappingNo{}Map child result paths back to parent. Uses dot-notation (e.g., nodes.step.output).
timeout_secondsNo300Maximum execution time for the child workflow
on_errorNoabortabort = propagate error, fallback = use fallback_value
fallback_valueNo{}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_b

Maximum nesting depth is 3 levels. Deeper nesting returns an error immediately.


Parallel Execution

A parallel node executes multiple branches concurrently and merges results.

Define parallel branches through edges with source_handle: parallel-*:

json
[
  { "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 -> End

Both branches connect to End, but only the taken branch executes. The end node sees results from whichever branch ran.


  • Iteration Nodes — For-each, filter, and reduce for array processing within workflows

Martha is built by aiaiai-pt.