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 / level —
system(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_inputand 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 does | Key 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 schedule | cron_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 |
| Manual | Run on demand from the UI | Input JSON (validated against input_schema) |
AI Operations
Node (type) | What it does | Key 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 prompt | prompt_id, model_type (vertex/openai/anthropic, default vertex), output_format (json/text), use_classification_prompt |
Summarization (summarization_task) | Generate a summary | summary_type (brief/detailed/bullet_points, default brief), max_length (words, default 500) |
LLM (llm_task) | Free-form text generation | prompt_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 node | classification_type, extract_after_classify (default true) |
Prompt Lab / RTIO (rtio_generation_task) | Generate a Role-Tasks-Instructions-Output prompt from a classified document | Requires 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 does | Key properties |
|---|---|---|
OCR (ocr_task) | Extract text from scanned PDFs / images | use_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 id | document_id |
Text → PDF (text_to_pdf_task) | Render text into a PDF artifact | title, font_size (default 20); outputs pdf_url |
DOCX Generation (docx_generate_task) | Create a DOCX from a template with data merge | template_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 fields | fields (defaults to common fields), validate_fields (default true), include_confidence (default false) |
Knowledge / RAG
Node (type) | What it does | Key properties |
|---|---|---|
RAG Enable (rag_enable_task) | Index a document for semantic search | document_id (required), user_id, force_reprocess (default false); outputs rag_status, rag_job_id |
RAG Poll Status (rag_poll_status_task) | Wait until indexing completes | Document id; polls rag_processing_status (often paired with a wait_task) |
RAG Query (rag_query_task) | Semantic search / Q&A over the knowledge base | query (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 does | Key properties |
|---|---|---|
Google Drive Fetch (google_drive_fetch_task) | Retrieve a Drive file by id or folder search | file_id, or folder_id + file_name (user OAuth) |
Gmail Fetch (gmail_fetch_task) | Read / filter emails | from, 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-thread | to + body (required), subject, thread_id, is_html |
REST API (rest_api_task) | Call any external HTTP API | url (required), method (default GET), headers, body, timeout (default 30s), output_mapping (JSONPath), oauth_integration |
Email Send (email_send_task) | Send SMTP email with {{variable}} substitution | to (array, required), subject + body (required), cc, bcc, from, html (default true) |
SharePoint Fetch (sharepoint_fetch_task) | List sites/files or fetch a document from SharePoint | action (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 server | mcp_server (required), tool_name (required), arguments, timeout, env_credentials |
Control Flow
Node (type) | What it does | Key properties |
|---|---|---|
Decision (decision / decision_task) | Conditional true/false branching | operator (==,!=,>,<,>=,<=,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 met | endpoint (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-execution | items (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 input | await_user_input, display_results, fields_to_show, display_format (default formatted), show_metadata (default true) |
Data Transform
Node (type) | What it does | Key properties |
|---|---|---|
Schema Validate (schema_validate_task) | Validate (and optionally coerce) a payload against a JSON Schema | schema_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 Mapping | Resolve {{var}} references from context before a node runs | {{var}} references (every node config supports them) |
| Output Mapping | Lift result fields into named variables | JSONPath 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_365for SharePoint). At execution time the executor looks up the user'suser-integration, auto-refreshes an expiring token, and injectsoauth_access_tokeninto the node'sinput_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 anoauth_integrationfor a managed token.
| Node | Auth / credentials | Key config properties | Output |
|---|---|---|---|
Google Drive Fetch (google_drive_fetch_task) | User OAuth2 (Google) or service account | file_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_read | Email list + count |
Gmail Reply (gmail_reply_task) | User OAuth2 (Google) | to + body (required), subject, thread_id (preserves thread), is_html | Sent confirmation |
REST API (rest_api_task) | Custom headers and/or oauth_integration | url (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_path | File URL / site list / file list |
MCP Tool (mcp_tool_task) | Per-server env_credentials | mcp_server (required), tool_name (required), arguments, timeout | Tool 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.
| Parameter | Type | Default | Description |
|---|---|---|---|
name | string (required) | — | Workflow identity; slug is derived from it |
description | text | — | Intent / notes |
category / sub_category | string | — | Organizational grouping |
workflow_type | enum | document_processing | document_processing · data_analysis · report_generation · custom |
status | enum | draft | draft · active · archived (lifecycle) |
version | string | 1.0 | Definition version label |
level | enum | user | system (shared) · user (private) · internal (hidden/transient) |
owner | relation | — | Owning user (private/user-level workflows) |
workflow_definition | json (required) | — | The DAG: nodes, edges, input_schema |
input_schema | json | — | JSON Schema for expected workflow inputs |
output_schema | json | — | JSON Schema for expected workflow outputs |
webhooks | relation | — | Bound webhook triggers (see Event Bus) |
execution_deadline_seconds | integer | 1800 | Hard wall-clock timeout; clamped to [60, 14400] (max 4h). On exceed → failed with WORKFLOW_DEADLINE_EXCEEDED |
max_retries | integer | inherits (→ tenant → hardcoded 3) | Per-workflow override of per-step transient-error retries; clamped [0, 10]. Resolution: per-node → per-workflow → tenant → 3 |
data_shield_policy | json | null (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_quota | biginteger | null (tenant cap only) | Per-workflow monthly token cap (UTC calendar month) |
monthly_usd_quota | decimal | null (tenant cap only) | Per-workflow monthly USD cap |
quota_override_at | datetime | — | Set by an admin when overriding this workflow's quota |
Inside workflow_definition, two further runtime keys may appear:
| Key | Type | Default | Description |
|---|---|---|---|
deadline_seconds | integer | 600 | Per-definition between-step budget (distinct from the activity wall-clock deadline above) |
Per-node config.max_retries | integer | inherits | Overrides 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
versionas 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 toactiveonly after validation and a manual test run,archiveinstead of deleting so executions keep their definition. - Clone, don't edit, system workflows — a
system/template workflow is shared. Use instantiate to get a privateusercopy; 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_secondsto the slowest realistic run; setmonthly_*_quotaon costly LLM workflows. - Anti-patterns to avoid: strings where arrays are required; one giant node; editing a live
activeworkflow other users depend on; leavingdata_shield_policyunset on workflows that process PII.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Save/validate rejected: "does not include an input schema" | workflow_definition is missing input_schema | Always 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 save | Config 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_input | An input / file-upload node is waiting for a human | Provide input in the timeline; resume uses the correlation_token. Times out after the node's timeout (default 3600s) → failed |
A {{variable}} resolves empty / shows literally | The 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_EXCEEDED | Wall-clock exceeded execution_deadline_seconds, or retries exhausted | Raise 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:
- Build — drag a Webhook trigger and an Email Send node onto the canvas; wire the edge.
- Map a variable — put a webhook field into the email body with
{{field_name}}; settoas an array and fillsubject. - Validate — confirm real-time validation is green (no missing
input_schema, no required fields empty). - Save as
active. - Trigger manually — copy the generated curl sample and POST a payload (or run with custom JSON inputs from the UI).
- Watch the timeline — poll status; confirm each node goes
pending → running → completedand the per-node inputs/outputs are what you expect. - Confirm outputs — the final node's result appears under the execution's
outputs. - Negative test — remove a required field (e.g. clear
subject, or deleteinput_schema) and confirm validation blocks save/execution with a specific error.
Cross-links
- Task node types · Node catalog
- Event Bus — webhook / schedule / event triggers
- Data Shield —
data_shield_policyenforcement - Human in the loop · Agentic automation · Governance
- Workflows vs. agents vs. squads
APIs used
| Method | Path | Purpose |
|---|---|---|
GET | /api/workflows/task-types | List available task types (with config_schema) |
POST | /api/workflows/validate | Validate a workflow_definition before execution |
POST | /api/activities | Create the workflow (Activity) |
POST | /api/workflow-executions | Create an execution (backend node format) |
POST | /api/workflow-executions/:id/execute | Queue the execution to the router |
GET | /api/workflow-executions/:id/status | Poll execution status + outputs |
POST | /api/webhook/:path | Trigger a workflow via webhook |
POST | /api/activities/:id/instantiate | Clone a system workflow into a private copy |
POST | /api/workflow-templates/:id/instantiate | Create a workflow from a template |