TurfAITurfAI User Guide
Modules

Task Node Mechanics

The universal anatomy of a task node and how data threads through one.

What it is

Every step you drop on the Visual Workflow Builder canvas is a task node — the same JSON shape regardless of whether it classifies a document, calls a REST endpoint, or sends an email. This page is the deep reference for that shared shape: how a node is structured, how {{variable}} inputs resolve, how a raw task result becomes named output variables, and how failures, retries, and async waits behave.

This page does not re-list the node catalog. For which nodes exist and their per-node config fields, see the full palette in Visual Workflow Builder and the Node catalog. Here we cover the mechanics those pages assume.

When to use it

Read this when you are wiring nodes together and need to know exactly how {{node.field}} resolves, why a variable came through empty or as a literal {{...}}, how to reshape a task's output, or what happens to the rest of the graph when one node fails. If you only need to pick a node and fill in its form, the catalog is enough.

How it works

Process flow — the life of a single node, from inputs to context:

Call flow — how the executor reaches a processor and threads the result back. The DMS transforms the frontend node format to the backend format, the Router enqueues the job, and a typed processor returns a result that matches the node's output_schema (per communication-standard):

Sub-features

  • Universal node anatomyid, type, data.label, data.task_type, data.config, plus the workflow-level input_schema/output_schema.
  • Frontend vs. backend format — the canvas writes type: "<task_type>"; the executor needs type: "task" with the real type in data.task_type. The DMS transforms it.
  • Input mapping{{variable}} templating across every string in config, resolved from upstream node outputs, trigger inputs, and built-in context vars.
  • Output mapping — JSONPath ($.field) that lifts fields out of a raw result into named variables downstream nodes can reference.
  • Per-node error policymax_retries, failure propagation, decision-gate routing, and the wait/poll node for async deadlines.

Node anatomy — the common shape

Every task node is the same envelope. The bold fields below are present on all node types; only data.config varies by task_type.

{
  "id": "classify-1",
  "type": "classification_task",
  "position": { "x": 120, "y": 80 },
  "data": {
    "label": "Classify Document",
    "task_type": "classification_task",
    "config": {
      "classification_type": "document_type",
      "include_confidence": true
    }
  }
}

The workflow as a whole also carries an input_schema (and usually an output_schema):

{
  "nodes": [ /* ... */ ],
  "edges": [ /* ... */ ],
  "input_schema": { "type": "object", "properties": {}, "required": [] }
}

Frontend vs. backend format (and the transform)

The canvas and the executor disagree on one field — type:

node.typenode.data.task_type
Frontend (canvas)"classification_task" (the real type)"classification_task"
Backend (executor)"task" (generic)"classification_task" (the real type)

The executor filters nodes by type === "task" and reads data.task_type to pick the processor. The DMS applies the transform before enqueuing — conceptually:

nodes.map(node => ({ ...node, type: "task" }))
// data.task_type is left untouched as the real type

If a node reaches the executor with type: "classification_task" instead of type: "task", the processor silently skips it and logs "Executing workflow with 0 steps" — the run completes with no outputs. This is almost always a hand-built or imported definition that missed the transform.

Input mapping & data flow

Any string inside data.config may contain {{variable}} references. Before the processor runs, the executor replaces each {{name}} with a value resolved from, in order:

  1. Upstream node outputs — fields the source node produced (and named via output_mapping).
  2. Trigger / workflow inputs — the inputs object passed at execution time (webhook payload, manual run form, schedule context).
  3. Built-in context vars — e.g. {{user_id}}, {{execution_id}}, {{workflow_id}}.

The substitution is literal string replacement: if a name does not resolve, the literal {{name}} is left in place rather than becoming empty — a useful tell when debugging.

End-to-end: classify → decide → extract

Three nodes threaded together. Node 1 classifies, names its result fields with output_mapping; node 2 (decision) routes on a classified field; node 3 extracts only on the "Invoice" branch and references the same field.

{
  "nodes": [
    {
      "id": "classify",
      "type": "classification_task",
      "data": {
        "label": "Classify",
        "task_type": "classification_task",
        "config": {
          "classification_type": "document_type",
          "output_mapping": {
            "doc_type": "$.classification.type",
            "doc_confidence": "$.confidence"
          }
        }
      }
    },
    {
      "id": "is-invoice",
      "type": "decision",
      "data": {
        "label": "Is Invoice?",
        "task_type": "decision",
        "config": {
          "operator": "==",
          "left_operand": "$.classify.doc_type",
          "right_operand": "Invoice"
        }
      }
    },
    {
      "id": "extract",
      "type": "extraction_task",
      "data": {
        "label": "Extract Invoice",
        "task_type": "extraction_task",
        "config": {
          "output_format": "json",
          "prompt_text": "Extract line items for a {{doc_type}} document.",
          "output_mapping": { "invoice_total": "$.extraction_result.total" }
        }
      }
    }
  ],
  "edges": [
    { "id": "e1", "source": "classify", "target": "is-invoice" },
    { "id": "e2", "source": "is-invoice", "target": "extract",
      "sourceHandle": "true", "label": "true" }
  ]
}

How the references resolve at runtime:

  • classify runs, produces a raw result, and output_mapping lifts doc_type and doc_confidence out of it.
  • The decision node compares $.classify.doc_type (JSONPath into the upstream node's named output) against the literal "Invoice". JSONPath operands here read from the named task outputs, not from the raw result.
  • On the true edge, extract runs; its prompt_text template {{doc_type}} resolves to "Invoice", and its own output_mapping names invoice_total for any node after it.

Array-vs-string pitfall. Fields declared as arrays in a node's schema (e.g. an email to) must be arrays, even with one value: ["a@x.com"], never "a@x.com". A bare string is treated character-by-character — the UI will report something like "17 recipients" and delivery breaks. When a template like {{recipient}} feeds an array field, make sure the upstream value is itself a list.

Per-node config & output_mapping

A processor returns a raw result shaped by the node's output_schema. output_mapping (JSONPath, $. root) selects pieces of that raw result and promotes them to named variables that downstream {{...}} templates and JSONPath operands can reach. Without an output_mapping, downstream nodes must navigate the raw shape directly.

Example — an extraction node. Raw result:

{
  "extraction_result": {
    "name": "John Doe",
    "email": "john@example.com",
    "skills": ["Python", "ML"]
  },
  "confidence": 0.92,
  "tokens_used": 1250
}

Config with output_mapping:

{
  "config": {
    "output_format": "json",
    "output_mapping": {
      "candidate_name": "$.extraction_result.name",
      "candidate_email": "$.extraction_result.email",
      "match_confidence": "$.confidence"
    }
  }
}

Now {{candidate_name}} resolves to "John Doe" in any downstream node, instead of forcing that node to reach into $.extraction_result.name. JSONPath also indexes arrays ($.items[0].id) and nested objects ($.response.data.status).

Error handling, retries & timeouts

Failure handling lives partly on the node and partly in the graph:

  • max_retries — a node may be configured to retry its processor call before giving up. Use it for flaky network nodes (REST, integrations), not for deterministic transforms.
  • Failure propagation — when a node fails (and exhausts retries), the executor marks that task_states[node_id] as "failed", sets the whole execution to status: "failed", records the failed_node and error, and stops — downstream nodes do not run. There is no implicit "continue on error".
  • Decision-gate routing on failure — to keep going after an expected error, route around it: have an upstream node emit a status field and a decision node send the failure case down a recovery branch rather than letting the node hard-fail.
  • Async waits & deadline budget — for work that completes later (RAG indexing, an external job), use a wait/poll node: it polls an endpoint's watch_field until it hits success_value, fails on failure_values, and gives up after timeout seconds (the deadline budget). This keeps long-running steps from blocking indefinitely.

A failed execution surfaces like this:

{
  "status": "failed",
  "error": "REST API call failed: 404 Not Found",
  "failed_node": "fetch-role-config",
  "task_states": {
    "fetch-role-config": { "status": "failed", "error": "404 Not Found" }
  }
}

Configuration parameters

These are the cross-cutting fields on every task node, regardless of type. Per-node config field schemas live in the Node catalog — link, don't duplicate.

FieldTypeMeaning
idstringUnique node id within the workflow; the key under which task_states and outputs are stored.
typestring"task" at execution time (the generic type); the specific type on the canvas.
data.labelstringDisplay name on the canvas; cosmetic.
data.task_typestringThe real task type — selects the processor (e.g. classification_task).
data.configobjectPer-node settings; every string value supports {{variable}} templates.
data.config.output_mappingobjectJSONPath ($.…) map from the raw result to named output variables.
data.config.max_retriesnumberRetries before the node fails (use for network-bound nodes).
input_schemaobjectWorkflow-level, required; declares the inputs a run accepts. Omitting it 400s the execution.
output_schemaobjectWorkflow-level declaration of the final output shape.

Dependencies

  • DMS (workflow-builder service) — owns task-type definitions and performs the frontend→backend node transform before enqueuing.
  • Router + Redisworkflow_queue carries the job; results_queue returns status, outputs, and task_states.
  • Typed processors — one per task family (extraction, classification, summarization, decision, wait, rest_api, email…), selected by data.task_type.
  • Upstream nodes & edges — a node's inputs are assembled from its incoming edges; an edge's sourceHandle (default "output") names the slice of the source result passed on.

Limitations

  • A node's raw output shape is fixed by its output_schema. Reshape with output_mapping, not by editing the node.
  • No continue-on-error. Any unhandled node failure fails the whole run; recovery must be modeled explicitly with decision branches.
  • Unresolved {{var}} stays literal. A missing variable is not an error — the literal {{var}} passes through, which can produce confusing downstream results.
  • Acyclic only. Decision nodes diverge and re-converge but the graph stays a DAG — no loops.

How to test

  1. Confirm the node type exists: GET /api/workflows/task-types lists it with its config/input/output schemas.
  2. Drop the node and verify the config panel renders the fields from its config_schema.
  3. Run the node in isolation (node test modal / test-llm-node) and compare the raw result to its output_schema.
  4. Verify the transform: the saved/exported definition has type: "task" and the real type in data.task_type for every node (else "0 steps").
  5. Verify input mapping: every {{var}} in a node's config resolves — no literal {{...}} left in the rendered config or the result.
  6. Verify output mapping: each output_mapping key appears as a named variable in the node's result; downstream {{key}} references resolve.
  7. Verify array fields: multi-value fields (e.g. email to) are JSON arrays, not strings.
  8. Verify failure path: force an error (bad URL, wrong id) and confirm status: "failed", the right failed_node, and that any intended decision-branch recovery fires.
  9. Verify async: for wait/poll nodes, confirm success on the target value and a clean timeout failure when the deadline passes.

Tips & best practices

  • Name outputs at the source. Add an output_mapping to any node whose result is consumed later — {{candidate_name}} is clearer and more stable than $.extraction_result.name.
  • Map once, reference everywhere. Promote a field to a named variable in the producing node rather than re-deriving it in each consumer.
  • Keep arrays arrays. When templating into array fields, ensure the source value is a list.
  • Budget every async step. Always set a timeout (and sensible poll_interval) on wait/poll nodes; never poll unbounded.
  • Model recovery, don't hope for it. Use decision gates for expected failures instead of relying on retries to mask them.

Troubleshooting

SymptomLikely causeFix
Variable renders as literal {{name}}Name never resolved (typo, or upstream never produced it)Check the producing node ran and that its output_mapping (or trigger input) defines name.
Variable resolves emptyField exists but is null/absent in the raw resultConfirm the JSONPath in the upstream output_mapping points at the real path.
Workflow logs "0 steps"Node type is the specific type, not "task"Re-run through the DMS transform so type: "task" and data.task_type is the real type.
output_mapping returns nothingJSONPath doesn't match the raw result shapeInspect the node's raw result; fix the $.… path to match output_schema.
"N recipients" where N is char countArray field given a stringSend ["a@x.com"], not "a@x.com".
Whole run fails after one nodeA node failed and max_retries was exhaustedInspect failed_node/error; add a decision recovery branch or raise max_retries for flaky calls.

APIs used

MethodPathPurpose
GET/api/workflows/task-typesList task types with config/input/output schemas.
POST/api/workflows/validateValidate a workflow definition (requires input_schema).
POST/api/workflow-executionsCreate an execution (nodes in backend type: "task" format).
POST/api/workflow-executions/:id/executeQueue the run.
GET/api/workflow-executions/:id/statusPoll status, outputs, task_states.
POST/api/workflow-executions/test-llm-nodeTest an AI/agent node in isolation.

See also

On this page