TurfAITurfAI User Guide
Modules

Visual Workflow Builder

The drag-and-drop canvas where workflows are designed and run.

What it is

The Visual Workflow Builder is a drag-and-drop canvas (powered by React Flow) for designing automation workflows. You drag nodes from a searchable palette, wire their inputs and outputs with {{variable}} references, configure each node in a context-aware panel, and run the result manually or via a trigger — watching a step-by-step execution timeline.

Under the hood a workflow is an Activity (Strapi collection) whose workflow_definition is a JSON DAG of nodes and edges, paired with an input_schema and output_schema. The canvas is the authoring surface; the Processors are the runtime that walks the graph.

When to use it

Use the builder whenever the steps of an automation are known and orderable — upload → classify → route → email. It is the most auditable unit of automation. Drop agent or squad nodes into the graph for the steps that need open-ended judgement. See workflows vs. agents vs. squads.

Prefer a workflow when you can draw the flowchart. Prefer an agent or squad when the next step depends on reasoning over the data. You can mix both in one graph.

How it works

Process flow — authoring and running a workflow:

Call flow — what happens on execution (DMS → Router → Processors, per communication-standard):

Execution status state machine — what the timeline reflects (a node-level lifecycle of pending → running → completed | failed; the execution rolls these up):

The frontend (React Flow) and backend (Processor) use different node shapes. On the canvas node.type is the specific task type (e.g. email_send_task); before execution the UI rewrites every node to the generic node.type: "task" and carries the real type in node.data.task_type. The processor only runs nodes whose type === "task". This transform is automatic — but it explains "Executing workflow with 0 steps" if a definition reaches the processor with specific types still set.

Sub-features

  • Searchable node palette with 30+ task types across seven categories (see The node palette below, plus Node catalog and Task node types).
  • Dynamic config panel — forms adapt to each node type from its config_schema.
  • Real-time validation before execution; auto-layout of nodes.
  • Execution timeline — per-node inputs/outputs, node status (active/running, skipped, failed, completed).
  • Status lifecycle — draft, active, archived.
  • Ownership / levelsystem (shared), user (private), internal (hidden transient); clone a system workflow via instantiation to get a private copy.
  • Control flow — decision nodes (true/false branches, labelled edges), wait/poll, for-each loops (see Task node types).
  • Human-in-the-loop — input/file-upload nodes pause the run as awaiting_user_input and resume with a correlation token (see Human in the loop).

The node palette

Everything you can drop on the canvas, grouped by the palette's categories. Each node's canvas node.type is the *_task identifier shown in the table; before execution the UI rewrites it to the generic type: "task" and moves the real type into data.task_type (see the callout above). The Key properties column lists the load-bearing config fields — the config panel renders the full schema from GET /api/workflows/task-types.

This palette section is self-complete for the common config. For the exhaustive per-field schemas (every input/output field, defaults, examples) see Node catalog and Task node types — the always-current source is GET /api/workflows/task-types and the synced feature reference.

Triggers

How a run starts. The trigger node carries trigger_type; the actual binding (path, cron, event source) lives on the Event Bus. Trigger nodes shape the start of the graph rather than dispatching as a task.

Node (type)What it doesKey properties
Webhook (trigger, trigger_type: webhook)Start from an HTTP POST (JSON, form-data, file upload)Path, payload type, secret key, max file size, MIME filters (on the webhook record)
Scheduled (trigger, trigger_type: schedule)Start on a cron schedulecron_expression, timezone, pause/resume, skip-if-running
Event Source (trigger, trigger_type: event)Start from an event (e.g. Drive folder watch)event_source_id, event_types (default ["*"]), custom criteria
ManualRun on demand from the UIInput JSON (validated against input_schema)

AI Operations

Node (type)What it doesKey properties
Classification (classification_task)Categorize a document (type / sentiment / topic)classification_type (default document_type), include_confidence (default true), return_multiple (default false)
Extraction (extraction_task)Pull structured fields with a promptprompt_id, model_type (vertex/openai/anthropic, default vertex), output_format (json/text), use_classification_prompt
Summarization (summarization_task)Generate a summarysummary_type (brief/detailed/bullet_points, default brief), max_length (words, default 500)
LLM (llm_task)Free-form text generationprompt_id or prompt_text, model_type (default vertex), process_combined_inputs (default true), output_format (default json)
Classify + Extract (classify_and_extract)Classify then conditionally extract in one nodeclassification_type, extract_after_classify (default true)
Prompt Lab / RTIO (rtio_generation_task)Generate a Role-Tasks-Instructions-Output prompt from a classified documentRequires metadata.classification_result and metadata.rtio_meta_prompt; outputs rtio_prompt, confidence
Agent (agent_task)Run a goal-driven agent (Agents)agent_id or inline goal, available_tools, model (default vertex), max_iterations (default 10, capped 20), temperature (default 0.3), max_tokens (default 4096), conversational, session_id, system_prompt, guardrails, mcp_servers

Document

Node (type)What it doesKey properties
OCR (ocr_task)Extract text from scanned PDFs / imagesuse_google_ocr (default true), language (default eng/en), dpi (default 300), chunk_size, max_workers
Document Input (document_input_task)Load an existing document into the run by iddocument_id
Text → PDF (text_to_pdf_task)Render text into a PDF artifacttitle, font_size (default 20); outputs pdf_url
DOCX Generation (docx_generate_task)Create a DOCX from a template with data mergetemplate_source (url/document/content/inline/pdf), template_url/template_document_id/template_content/template_inline, output_filename; reports missing_variables
Metadata Extraction (metadata_extraction_task)Pull document metadata fieldsfields (defaults to common fields), validate_fields (default true), include_confidence (default false)

Knowledge / RAG

Node (type)What it doesKey properties
RAG Enable (rag_enable_task)Index a document for semantic searchdocument_id (required), user_id, force_reprocess (default false); outputs rag_status, rag_job_id
RAG Poll Status (rag_poll_status_task)Wait until indexing completesDocument id; polls rag_processing_status (often paired with a wait_task)
RAG Query (rag_query_task)Semantic search / Q&A over the knowledge basequery (required), user_id, top_k (default 5), similarity threshold, reranking, session_id; outputs answer, sources[], session_id

Integration

See Integration nodes below for full properties (auth, URL/method/body, output mapping). Summary:

Node (type)What it doesKey properties
Google Drive Fetch (google_drive_fetch_task)Retrieve a Drive file by id or folder searchfile_id, or folder_id + file_name (user OAuth)
Gmail Fetch (gmail_fetch_task)Read / filter emailsfrom, to, subject, label, query, is_unread, has_attachment, after/before, max_results (default 10), download_attachments, mark_as_read
Gmail Reply (gmail_reply_task)Reply in-threadto + body (required), subject, thread_id, is_html
REST API (rest_api_task)Call any external HTTP APIurl (required), method (default GET), headers, body, timeout (default 30s), output_mapping (JSONPath), oauth_integration
Email Send (email_send_task)Send SMTP email with {{variable}} substitutionto (array, required), subject + body (required), cc, bcc, from, html (default true)
SharePoint Fetch (sharepoint_fetch_task)List sites/files or fetch a document from SharePointaction (fetch_file/list_sites/list_files, default fetch_file), site_id, item_id, folder_path (Microsoft OAuth)
MCP Tool (mcp_tool_task)Invoke a tool on an MCP servermcp_server (required), tool_name (required), arguments, timeout, env_credentials

Control Flow

Node (type)What it doesKey properties
Decision (decision / decision_task)Conditional true/false branchingoperator (==,!=,>,<,>=,<=,in,contains,matches), left_operand (JSONPath or literal), right_operand, true_label/false_label. Must have exactly 2 labelled outgoing edges
Wait / Poll (wait_task)Poll an endpoint until a condition is metendpoint (required), method (default GET), watch_field (required), success_value (default completed), failure_values (default ["failed","error"]), poll_interval (default 3s), timeout (default 120s)
For-Each (for_each_task)Iterate an array, per-item sub-executionitems (array ref), sub_tasks, item_variable (default item), mode (sequential/parallel), max_parallel (default 10), accumulate
Squad (squad_task)Run a multi-agent squad (Squads)squad_id, or inline agents + tasks; process (sequential/hierarchical), manager_agent, max_total_iterations (default 30)
Results Display / HITL (results_display_task)Pause to show results / await human inputawait_user_input, display_results, fields_to_show, display_format (default formatted), show_metadata (default true)

Data Transform

Node (type)What it doesKey properties
Schema Validate (schema_validate_task)Validate (and optionally coerce) a payload against a JSON Schemaschema_source (inline/document_id/url/template, default inline), schema/schema_document_id/schema_url/template_id, coerce (default true), payload_var (default {{result}}), max_retries (default 2)
Input MappingResolve {{var}} references from context before a node runs{{var}} references (every node config supports them)
Output MappingLift result fields into named variablesJSONPath expressions ($.path.to.field)

Built-in context variables: {{user_id}}, {{execution_id}}, {{DMS_URL}}, plus all webhook form fields. File uploads produce {field}_document_id, {field}_file_url, {field}_mime_type.

Coming soon (planned, not yet on the palette): the integration framework supports adding providers by OAuth config alone — Microsoft 365 / Outlook, OneDrive, Teams, Salesforce, Slack, and other connectors are seeded or in progress via the generic OAuth2 adapter but may not all be wired as dedicated nodes yet. The shipped integration nodes are the seven listed above (Drive, Gmail fetch/reply, REST API, Email Send, SharePoint, MCP Tool).

Integration nodes

Integration nodes connect a workflow to the outside world. They share one pattern: the node config names the operation, the executor resolves {{variables}} and injects credentials, and the processor calls a DMS service that holds the OAuth tokens and makes the provider API call. Credentials are never stored in the workflow definition.

How credentials reach a node:

  • Google (Drive / Gmail) — user-delegated OAuth2 (or service account); the DMS Google OAuth service injects the token. The user connects on the Integrations page first.
  • Generic OAuth2 providers — set "oauth_integration": "<provider>" on the node (e.g. microsoft_365 for SharePoint). At execution time the executor looks up the user's user-integration, auto-refreshes an expiring token, and injects oauth_access_token into the node's input_mapping.
  • SMTP email — server-side credentials from environment, no per-user OAuth.
  • REST API — bring your own auth via headers (e.g. Authorization: Bearer {{api_token}}), or attach an oauth_integration for a managed token.
NodeAuth / credentialsKey config propertiesOutput
Google Drive Fetch (google_drive_fetch_task)User OAuth2 (Google) or service accountfile_id (direct), or folder_id + file_name (search mode)file_url (GCS), file_id, file_name, mime_type, size, document_id
Gmail Fetch (gmail_fetch_task)User OAuth2 (Google)Filters: from, to, subject, label, query, is_unread, has_attachment, after/before; max_results (10), download_attachments, mark_as_readEmail list + count
Gmail Reply (gmail_reply_task)User OAuth2 (Google)to + body (required), subject, thread_id (preserves thread), is_htmlSent confirmation
REST API (rest_api_task)Custom headers and/or oauth_integrationurl (required, supports templates), method (GET), headers, body (POST/PUT/PATCH), timeout (30s), output_mapping (JSONPath → named vars)data, status_code, _raw_response + mapped fields
Email Send (email_send_task)Server SMTP (env)to (array, required), subject + body (required), cc, bcc, from, html (true)sent, recipients, timestamp
SharePoint Fetch (sharepoint_fetch_task)Microsoft OAuth2 via oauth_integration (+ Strapi JWT for fetch_file)action (fetch_file/list_sites/list_files), site_id, item_id, folder_pathFile URL / site list / file list
MCP Tool (mcp_tool_task)Per-server env_credentialsmcp_server (required), tool_name (required), arguments, timeoutTool result

email_send (and Gmail) recipient fields are arrays, not comma strings — "to": ["a@x.com"], never "to": "a@x.com". URLs, headers, and bodies on the REST/Email nodes all support {{variable}} substitution; use output_mapping (JSONPath, e.g. "jd_file_id": "$.data.jd_file_id") to lift nested response fields into named variables for downstream nodes.

For the full integration architecture (OAuth flow, token injection, the generic adapter, and the roadmap of additional connectors) see Integrations.

Configuration parameters

The Activity (workflow_definition owner) carries these fields. Reconciled against the DMS activity schema — type and default included.

ParameterTypeDefaultDescription
namestring (required)Workflow identity; slug is derived from it
descriptiontextIntent / notes
category / sub_categorystringOrganizational grouping
workflow_typeenumdocument_processingdocument_processing · data_analysis · report_generation · custom
statusenumdraftdraft · active · archived (lifecycle)
versionstring1.0Definition version label
levelenumusersystem (shared) · user (private) · internal (hidden/transient)
ownerrelationOwning user (private/user-level workflows)
workflow_definitionjson (required)The DAG: nodes, edges, input_schema
input_schemajsonJSON Schema for expected workflow inputs
output_schemajsonJSON Schema for expected workflow outputs
webhooksrelationBound webhook triggers (see Event Bus)
execution_deadline_secondsinteger1800Hard wall-clock timeout; clamped to [60, 14400] (max 4h). On exceed → failed with WORKFLOW_DEADLINE_EXCEEDED
max_retriesintegerinherits (→ tenant → hardcoded 3)Per-workflow override of per-step transient-error retries; clamped [0, 10]. Resolution: per-node → per-workflow → tenant → 3
data_shield_policyjsonnull (no policy)Workflow-level Data Shield policy; when required: true every llm_task/agent_task must have Data Shield enabled or the run fails with DATA_SHIELD_POLICY_VIOLATION
monthly_token_quotabigintegernull (tenant cap only)Per-workflow monthly token cap (UTC calendar month)
monthly_usd_quotadecimalnull (tenant cap only)Per-workflow monthly USD cap
quota_override_atdatetimeSet by an admin when overriding this workflow's quota

Inside workflow_definition, two further runtime keys may appear:

KeyTypeDefaultDescription
deadline_secondsinteger600Per-definition between-step budget (distinct from the activity wall-clock deadline above)
Per-node config.max_retriesintegerinheritsOverrides retry count for a single node

Examples

A minimal three-node DAG — classify → decision → extract — in canvas (frontend) format. Note the labelled decision edges and the always-present input_schema.

{
  "nodes": [
    {
      "id": "classify-1",
      "type": "classification_task",
      "position": { "x": 80, "y": 120 },
      "data": {
        "label": "Classify document",
        "task_type": "classification_task",
        "config": { "document_types": ["invoice", "receipt", "other"] }
      }
    },
    {
      "id": "decide-1",
      "type": "decision_task",
      "position": { "x": 360, "y": 120 },
      "data": {
        "label": "Is it an invoice?",
        "task_type": "decision_task",
        "config": {
          "left": "{{classify-1.category}}",
          "operator": "==",
          "right": "invoice"
        }
      }
    },
    {
      "id": "extract-1",
      "type": "extraction_task",
      "position": { "x": 640, "y": 40 },
      "data": {
        "label": "Extract invoice fields",
        "task_type": "extraction_task",
        "config": { "output_format": "json" }
      }
    }
  ],
  "edges": [
    { "id": "e1", "source": "classify-1", "target": "decide-1" },
    { "id": "e2", "source": "decide-1", "target": "extract-1", "sourceHandle": "true", "label": "true" }
  ],
  "input_schema": {
    "type": "object",
    "properties": { "file_url": { "type": "string" } },
    "required": ["file_url"]
  }
}

Variable / data-flow example. A downstream node config pulls a field off an upstream result. {{variable}} substitution is literal string replacement against the resolved context; JSONPath (e.g. on a REST API node's output_mapping) lifts a nested field into a named variable:

{
  "subject": "Invoice {{extract-1.invoice_number}} received",
  "body": "Total due: {{extract-1.total_amount}} on {{extract-1.due_date}}"
}

Array fields must be JSON arrays, not comma strings — "to": ["a@x.com"], never "to": "a@x.com" (a string is counted as N characters, so the UI reports "17 recipients"). Unresolved {{vars}} are left literally in place, which is the usual cause of an empty- or template-looking output.

Dependencies

  • DMS (Strapi) — the API, the Activity store, and the job submitter the UI talks to.
  • Router (Redis-backed) — enqueues jobs onto workflow_queue; forwards the DMS payload unchanged.
  • Processors — walk the DAG in topological order and execute individual node tasks.
  • Per-node services — LLM service, RAG query service, integrations, as the nodes require.

Limitations

  • A workflow is only as adaptive as its nodes; for open-ended steps you must add agents/squads.
  • Draft workflows skip strict validation (used by the Composer); active workflows are validated.
  • The processor runs only nodes with type: "task"; input/output/decision-only nodes shape flow but aren't dispatched as tasks.
  • A node config that references {{vars}} that never resolve does not fail — it emits the literal template (see Troubleshooting).
  • Coming soon: version history / undo-redo on the canvas is planned, not yet shipped; treat version as a manual label and clone before risky edits.

Tips & best practices

  • Naming — give workflows and nodes verb-first, human-readable names ("Classify document", not "node-3"). The label is what the timeline shows.
  • Lifecycle discipline — author in draft, promote to active only after validation and a manual test run, archive instead of deleting so executions keep their definition.
  • Clone, don't edit, system workflows — a system/template workflow is shared. Use instantiate to get a private user copy; edit that.
  • Keep nodes small — one responsibility per node makes the timeline and retries meaningful. Split a do-everything LLM node into classify → extract.
  • Variable hygiene — name inputs in input_schema, reference them exactly, and prefer explicit output mapping (JSONPath) over deep {{a.b.c}} chains.
  • Set deadlines and quotas deliberately — tune execution_deadline_seconds to the slowest realistic run; set monthly_*_quota on costly LLM workflows.
  • Anti-patterns to avoid: strings where arrays are required; one giant node; editing a live active workflow other users depend on; leaving data_shield_policy unset on workflows that process PII.

Troubleshooting

SymptomCauseFix
Save/validate rejected: "does not include an input schema"workflow_definition is missing input_schemaAlways include input_schema: { type: "object", properties: {}, required: [] }
"Executing workflow with 0 steps"Nodes reached the processor with specific type instead of type: "task"Ensure the frontend→backend transform ran; specific type belongs in data.task_type
Node config invalid on saveConfig doesn't match the node's config_schema (e.g. missing required to/subject, wrong field type)Fill required fields; use arrays for array fields; check the config panel for inline errors
Execution stuck in awaiting_user_inputAn input / file-upload node is waiting for a humanProvide input in the timeline; resume uses the correlation_token. Times out after the node's timeout (default 3600s) → failed
A {{variable}} resolves empty / shows literallyThe referenced key isn't in the resolved context (typo, upstream node not completed, wrong path)Substitution leaves unknown {{vars}} in place — verify the upstream node id/field and that it ran first
Run ends failed with WORKFLOW_DEADLINE_EXCEEDEDWall-clock exceeded execution_deadline_seconds, or retries exhaustedRaise the deadline (≤ 4h), simplify slow nodes, or raise per-node/max_retries for flaky external calls

How to test

A repeatable checklist for a positive then a negative run:

  1. Build — drag a Webhook trigger and an Email Send node onto the canvas; wire the edge.
  2. Map a variable — put a webhook field into the email body with {{field_name}}; set to as an array and fill subject.
  3. Validate — confirm real-time validation is green (no missing input_schema, no required fields empty).
  4. Save as active.
  5. Trigger manually — copy the generated curl sample and POST a payload (or run with custom JSON inputs from the UI).
  6. Watch the timeline — poll status; confirm each node goes pending → running → completed and the per-node inputs/outputs are what you expect.
  7. Confirm outputs — the final node's result appears under the execution's outputs.
  8. Negative test — remove a required field (e.g. clear subject, or delete input_schema) and confirm validation blocks save/execution with a specific error.

APIs used

MethodPathPurpose
GET/api/workflows/task-typesList available task types (with config_schema)
POST/api/workflows/validateValidate a workflow_definition before execution
POST/api/activitiesCreate the workflow (Activity)
POST/api/workflow-executionsCreate an execution (backend node format)
POST/api/workflow-executions/:id/executeQueue the execution to the router
GET/api/workflow-executions/:id/statusPoll execution status + outputs
POST/api/webhook/:pathTrigger a workflow via webhook
POST/api/activities/:id/instantiateClone a system workflow into a private copy
POST/api/workflow-templates/:id/instantiateCreate a workflow from a template

On this page