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_nameYes—Name 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.


Authorization (run-as identity) ​

When a workflow has a function node (a node that calls a tool such as query_collection, generate_report, or any platform/HTTP function), that function executes under a run-as principal — the client of whoever triggered the workflow:

  • Triggered by a user (admin UI / CLI / API, no client selected) → runs as the tenant's default client.
  • Triggered by a service account (e.g. an integration calling POST /api/service/workflows/{name}/execute) → runs as that service account's client.

The function node is then authorized against that client's grants. So to let a workflow's query_collection node run, grant the function to the client, not the agent:

bash
martha clients grant <client> function query_collection

Agent grants don't apply to workflow function nodes. martha agents add-function <agent> <fn> grants a tool to an agent — that applies when the agent is calling the tool (in chat / an agent loop). A workflow function node does not run "as" an agent, so an agent grant has no effect there. Grant the client the workflow runs as.

If the run-as client isn't granted the function, the node fails with a clear 403 "not authorised" (not a generic error). Running a workflow also requires the client to have access to the workflow itself (martha clients grant <client> workflow <name>); user/admin runs see all tenant workflows.

This mirrors how automation platforms scope steps to a configured service identity rather than the runner's ambient permissions: a workflow's authority is the client it runs as, plus the functions that client is granted.

  • Iteration Nodes — For-each, filter, and reduce for array processing within workflows
  • CLI — clients grant / agents add-function for managing access
  • Document Ingestion — query_collection and the RAG retrieval functions used in workflow nodes

Martha is built by aiaiai-pt.