Elizon Docs
Workflows

Building a Workflow

The node types a workflow is built from, and how your input reaches them.

A workflow is a graph of nodes joined by edges. You assemble it in Studio; this page covers the parts that decide what your integration has to send and what it gets back.

Node types

NodeWhat it does
Manual triggerEntry point for a run started by hand in Studio, or through the API — see Manual trigger.
RecurrenceEntry point for a run started on a schedule — see Recurrence trigger.
HTTP RequestEntry point for a run started by an inbound webhook — see Webhook trigger. Also declares the payload the run must carry.
Form TriggerEntry point for a run started by a person submitting a hosted form — see Form trigger.
Email TriggerEntry point for a run started by a new email arriving in a mailbox you connect — see Email trigger.
AI AgentRuns one of your configured agents as a step — see AI Agent.
Team StepRuns a configured team as a step — see Team Step.
AI PromptA one-off prompt to a model, without a saved agent behind it — see AI Prompt.
ClassifyRoutes the run by classifying text into one of a set of labels — see Classify.
If / ElseBranches on a condition — see If / Else.
LoopIterates over an array, running its body once per item (several at once with batchSize) — see Loop.
WaitPauses the run for an interval, or until a date and time — see Wait.
VariableSets a named value that later nodes can read — see Variable.
Edit FieldsBuilds an object out of named fields, from expressions or fixed values — see Edit Fields.
Convert to FileTurns a value into a downloadable file — Markdown, text, JSON, CSV, HTML, PDF, Word or Excel. See Convert to File.
Send EmailSends an email through your own SMTP server, with files attached. See Send Email.
Data TableReads or writes a project's Data Table as a step — the register that lets a run remember. See Data Table.
HTTP RequestCalls any HTTP or REST API as a step — the same request every run, with no model deciding what to send. See HTTP Request.
MCP ToolCalls one tool on an MCP server you have connected, with arguments you set. See MCP Tool.
FTP / SFTPLists, downloads, uploads, moves and deletes files on a remote file server. See FTP / SFTP.
AppsOne entry per connected app — Google Calendar, Contacts, Sheets and Drive today. Reads and writes that app with a connected account. See Apps.
Human ReviewPauses the run for a person to approve — see Human review.
NoteA canvas annotation. Never executes — see Note.

Declaring what your payload has to look like

Payload declaration belongs to the trigger, not to a separate node. There is no Input node.

An HTTP Request trigger declares fields by name, type (string, number, boolean, or object), and whether each is required. Those declarations are checked before the first node runs, so a bad payload fails immediately and cheaply rather than halfway through a run that already cost you tokens.

If validation fails, the run is closed as failed and the error you get back names the offending fields.

The fields describe the request body, so a webhook call is validated against the body you POSTed rather than against the { body: … } envelope the run carries it in. A Studio manual run and a POST /workflows/:id/run call are validated against their payload directly.

An HTTP Request trigger that declares no fields accepts anything, including an empty payload — and so does a graph with no HTTP Request trigger at all.

The check runs on every run of that graph, whichever trigger started it. A graph that carries an HTTP Request trigger with required fields therefore fails a scheduled run (whose payload is empty) and a form submission (which is checked against the HTTP trigger's fields as well as the form's own) — if a graph is reachable from more than one trigger, declare only fields every path can supply.

A Form Trigger declares its own, richer field list (a label as well as a name, eleven field types, options for the choice types). Those are validated by the form's own ingress at submission time, so the person filling the form sees per-field errors on the page rather than a run that fails somewhere they will never look — see Triggers.

Referencing values: the {{ }} syntax

Node configuration fields can interpolate values from the run using {{ }} tokens. There are exactly five namespaces:

TokenReads
{{ trigger.<path> }}The payload the run was started with.
{{ upstream.<nodeId>.output }}The output of a node that has already run.
{{ variables.<name> }}A value set by a Variable node.
{{ loop.item }}, {{ loop.index }}The current element and position inside a Loop.
{{ run.trigger.type }}, {{ run.trigger.id }}Which trigger started this run.
{{ run.startedAt }}When this run started, as ISO 8601 UTC (…T07:30:00.000Z).

This is a substitution syntax, not an expression language — dot-path lookup only. There are no operators, no function calls, and no way to evaluate arbitrary code, by design.

To read an element of an array, use its position as a numeric dot segment, counting from 0: {{ trigger.items.0.sku }} reads the sku of the first item. A bracket index such as items[0] is not part of the syntax and fails the node with malformed_expression. An index past the end of the array fails the node just like a reference to a field that is not there. There are no negative indexes: items.-1 does not read the last item, it fails the same way as an index past the end.

run.* is the one namespace nothing a caller sends can write into: {{ run.trigger.type }} is one of manual, schedule, webhook, form, api, email, knowledge-document or app, and {{ run.trigger.id }} is the id of the trigger node that fired — so a graph reachable from more than one trigger can branch on how it was started. A payload field called run or type cannot change either value. run.trigger has exactly those two fields; any other run.* reference fails the node rather than resolving to nothing.

{{ run.startedAt }} is the moment the run started, in UTC — for example 2026-10-09T07:30:00.000Z. It is recorded once per run, so every node of the run (including a node that runs days later after a Wait or a Human Review) sees the same value. Use it to tell an AI Prompt or AI Agent node what "today" is: Today is {{ run.startedAt }}. Summarise the overdue invoices. Converting it to another time zone is up to the reader; a model handles the Z suffix fine. A run started before this field existed fails a {{ run.startedAt }} reference with missing_run_started_at.

trigger.* is shaped by which trigger started the run

A run started through POST /workflows/:id/run binds your request's input object directly, so {{ trigger.orderId }} reads input.orderId. A run started by a webhook binds the request body one level down, under body — so the same field is {{ trigger.body.orderId }}. A form submission uses that same body envelope, keyed by each field's name: {{ trigger.body.email }}. A scheduled run has an empty trigger payload. If a graph is meant to be reachable from more than one trigger, this difference is the thing to get right.

An unresolvable reference is a real error, not a silent empty string: a node that reads {{ variables.total }} where no such variable was ever set fails with a message naming the token, rather than proceeding on undefined.

Edit Fields: setting named values

An Edit Fields node builds an object out of named fields — the direct way to rename, reshape or assemble data mid-graph. See Edit Fields.

Convert to File: making a downloadable file

A Convert to File node turns a value into a Markdown, text, JSON, CSV, HTML, PDF, Word or Excel file kept with the run for 30 days. See Convert to File.

Send Email: sending the result

A Send Email node sends a message through your own SMTP server, with files attached. See Send Email.

HTTP Request: calling an API

An HTTP Request node calls any HTTP or REST API as a deterministic step — the same request every run. See HTTP Request.

MCP Tool: calling a connected integration

An MCP Tool node calls one tool on a connected MCP server, with arguments you set. See MCP Tool.

FTP / SFTP: exchanging files with a file server

An FTP / SFTP node lists, downloads, uploads, moves and deletes files on a remote file server. See FTP / SFTP.

Apps: Google Calendar, and the rest

The Apps row of the palette holds one entry per app — Google Calendar, Contacts, Sheets, Drive and, where the deployment offers it, Gmail. Each reads and writes that app as a connected account. See the Apps overview and each app's page.

What comes back

A run's output is derived from the graph, not assembled by a node you place. There is no Output node.

When a run ends, Elizon looks at the nodes that terminate the graph it actually walked — nodes with no outgoing edge, that completed:

  • Exactly one — its own output, unwrapped. That is any JSON value, not necessarily an object: a node whose output is a string returns a string.
  • Several — an object keyed by node id, so two independent end points are both reported rather than one of them silently winning.
  • None (every terminal node was skipped or errored, or the graph is empty) — output is null.

Note nodes never count, however they are placed on the canvas, and neither do the nodes inside a Loop's body — the Loop node itself is the end point of that path.

If you want a specific result shape from a synchronous call, end the graph on a node that produces it — an Edit Fields node is the direct way to do that.

Wait: pausing a run

A Wait node pauses a run for an interval, or until a date and time, durably — a restart or deploy does not lose it. See Wait.

Loops have a ceiling

A Loop node stops after a bounded number of iterations (100 by default) and ends the run as loop_limit_reached rather than failing it. See Loop.

Where to go next

  • Triggers — how a run actually starts.
  • Publishing — making the graph runnable from outside Studio.