Workflow nodes
The portable data of every Workflow node type, what travels in a Package, and a working example of each.
This page is the field reference for the data of each node in a Workflow definition ("what the file holds"). Each node type also has a page under Workflows that describes its panel in Studio ("how the panel behaves").
Every node section has three parts:
- Notes, written by hand: which fields take expressions, the node's outputs and edge handles, what travels in a Package, what is collected at install, and what
validateandbuildrefuse. - Fields, generated from the node's data schema. "In the file" says what the Package does with each field:
as writtentravels unchanged,as agent(and the like) means you write the reference instead of the stored id, andnever carriedmeans the field is left out of the file. Rows such asbranches[].idare the fields of each entry of a list;inputs.<name>.valueis a field of each entry of a map. - Example, the workflow file that
npx @elizon/cli example workflow --node <type>prints. The command prints it inside a complete project (the manifest and every file the node references) that passesvalidateandbuild.
For several nodes working together, run npx @elizon/cli example workflow --pattern <name>:
| Pattern | What it shows |
|---|---|
if-else | An If/Else node with two branches and a default, each wired by its sourceHandle. |
loop-send-email | A Data Table read, a Loop over the rows with a Send Email in its body, and a step after the loop. |
human-review-upsert | A single and a batch Human Review, each followed by a Data Table upsert with its key condition. |
code-bulk-upsert | A Code node that writes /workspace/out/rows.jsonl, and a Data Table bulkUpsert that reads the file by key. |
Expressions
A string field that takes expressions can hold {{ … }} references, resolved when the node runs: {{ trigger.body.orderId }}, {{ upstream.<node id>.output.<path> }}, {{ loop.item }}, {{ variables.<name> }}, {{ run.startedAt }}. See Referencing values.
Some rows (Edit Fields, Data Table mappings and conditions, App inputs, MCP Tool arguments, Knowledge Search filters, Human Review defaults) have a valueMode: expression resolves the value, fixed uses it exactly as written. Quote a YAML value that starts with {{, for example value: '{{ trigger.body.email }}'.
Edges and handles
An edge connects a source node to a target node. Most nodes have one output, and their edges have no sourceHandle. Three node types have named outputs, and an edge leaving one of them names the output in sourceHandle:
| Node | sourceHandle values |
|---|---|
if_else | The id of each entry of branches, and defaultBranchId (default unless you change it). Exactly one is taken. |
classify | category-<id> for each entry of categories (category-billing for id: billing), and category-other when noMatchAction is other_branch. |
loop | loop for the loop body (runs once per item) and done for what runs once after the last item. |
A Human Review node has one output, not one per decision: branch on its decision with an If/Else node, as the human-review-upsert pattern does. An edge's data is optional and only holds what the canvas records about the two ports (sourceType, targetType: text, json, agent_output or any; typeMismatch). Leave it out when you write a file by hand. targetHandle is not used.
- { id: order-to-route, source: order, target: route }
- { id: route-large, source: route, target: approval, sourceHandle: large }
- { id: route-default, source: route, target: standard, sourceHandle: default }
- { id: each-line-body, source: each-line, target: line-total, sourceHandle: loop }
- {
id: each-line-done,
source: each-line,
target: collect,
sourceHandle: done,
data: { sourceType: json, targetType: any },
}What validate and build refuse
These apply to every node. A node's own section lists the ones particular to it.
| Code | When |
|---|---|
UNRESOLVED_LOCAL_REFERENCE | validate and build: a reference (agents., teams., tools., knowledgeBases., dataTables.) names nothing the manifest declares. |
PORTABLE_UNSUPPORTED_NODE_TYPE | build: the type is not one of the 27, for example a retired type from an old saved version. |
PORTABLE_DEFINITION_INVALID | build: a key the node does not carry (including the stored-only ones, such as agentId), a value Studio could not save (an unknown enum value, a number out of range), an identifier-shaped (UUID) value, a duplicate node or edge id, or an edge to a node that is not in the graph. validate and build: a second Webhook, Form, Email, App or Recurrence trigger in one workflow. |
Triggers
A workflow holds at least one trigger. A run starts from the trigger that fired, and a workflow can hold any number of Manual triggers but at most one Webhook, Form, Email, App and Recurrence trigger each. A trigger has no input handle.
trigger_manual
The Manual trigger (node page) starts a run on demand.
- Expressions: none. Later nodes read the run's input flat, as
{{ trigger.<field> }}. - Handles: one output.
- In a Package:
descriptiontravels. Nothing is collected at install. - Refusals: only the ones every node has.
Fields (from ManualTriggerNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
description | string | no | '' | — | as written | A note for whoever runs it — what the workflow does and what input, if any, it expects. Shown on the node. |
Output
A trigger has no upstream output: later nodes read what started the run as {{ trigger.… }} and {{ run.trigger.… }}. The node page lists the paths.
Example (npx @elizon/cli example workflow --node trigger_manual prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Manual trigger example
description: An operator starts the run from the web app with a request.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: Paste the customer request as `request`.
- id: keep
type: edit_fields
position: { x: 280, y: 0 }
data:
rows:
- id: field-1
name: received
value: '{{ trigger.request }}'
valueMode: expression
keepOnlySetFields: true
edges:
- { id: start-to-keep, source: start, target: keep }trigger_recurrence
The Recurrence trigger (node page) starts a run on a schedule: every intervalValue intervalUnit in interval mode, or on a five-field cronExpression in cron mode, in timezone (a tz-database name such as Europe/Berlin).
- Expressions: none; a scheduled run's trigger payload is empty. Use
{{ run.startedAt }}for the time. - Handles: one output.
- In a Package: the whole schedule travels. Nothing is collected at install.
- Refusals: a second Recurrence trigger in the workflow (
PORTABLE_DEFINITION_INVALID).
Fields (from RecurrenceTriggerNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
mode | string | no | 'interval' | interval, cron | as written | Interval for "every N units", Cron for a specific wall-clock time. |
intervalValue | integer | no | 1 | ≥ 1 | as written | The N in "every N units". Interval mode only. |
intervalUnit | string | no | 'days' | minutes, hours, days, weeks | as written | The unit. Interval mode only. |
cronExpression | string | no | '' | — | as written | A five-field cron expression, for example 0 9 * * 1-5 (09:00 on weekdays). Cron mode only. |
timezone | string | no | 'UTC' | — | as written | The zone the schedule is expressed in, for example Europe/Sofia. The schedule keeps local time across daylight-saving changes. |
Output
A trigger has no upstream output: later nodes read what started the run as {{ trigger.… }} and {{ run.trigger.… }}. The node page lists the paths.
Example (npx @elizon/cli example workflow --node trigger_recurrence prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Recurrence trigger example
description: Runs at 08:00 Berlin time on weekdays.
graph:
nodes:
- id: every-morning
type: trigger_recurrence
position: { x: 0, y: 0 }
data:
mode: cron
cronExpression: 0 8 * * 1-5
timezone: Europe/Berlin
- id: keep
type: edit_fields
position: { x: 280, y: 0 }
data:
rows:
- id: field-1
name: received
value: '{{ run.startedAt }}'
valueMode: expression
keepOnlySetFields: true
edges:
- { id: every-morning-to-keep, source: every-morning, target: keep }trigger_http
The Webhook trigger (node page) starts a run from an HTTP request. fields declares the body it expects; each is checked on every call.
- Expressions: none on the node. Later nodes read the body as
{{ trigger.body.<name> }}. - Handles: one output.
- In a Package:
methodandfieldstravel.webhookSecretis never carried; the endpoint and its token are created when the installed workflow is published. - Refusals: a second Webhook trigger in the workflow (
PORTABLE_DEFINITION_INVALID).
Fields (from HttpRequestTriggerNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
method | string | no | 'POST' | GET, POST, PUT, PATCH, DELETE | as written | The HTTP method the trigger documents. |
webhookSecret | string | no | '' | — | never carried | The signing secret for HMAC-signed calls. Studio shows it next to the URL; treat it as a credential. |
fields | list of objects | no | [] | — | as written | The request-body fields. type is string, number, boolean or object. Studio shows the matching OpenAPI request body beside them. |
fields[].name | string | yes | — | — | as written | — |
fields[].type | string | yes | — | string, number, boolean, object | as written | — |
fields[].required | boolean | yes | — | — | as written | — |
Output
A trigger has no upstream output: later nodes read what started the run as {{ trigger.… }} and {{ run.trigger.… }}. The node page lists the paths.
Example (npx @elizon/cli example workflow --node trigger_http prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Webhook trigger example
description: Takes an order the shop posts.
graph:
nodes:
- id: order-received
type: trigger_http
position: { x: 0, y: 0 }
data:
method: POST
fields:
- name: orderId
type: string
required: true
- name: total
type: number
required: true
- id: keep
type: edit_fields
position: { x: 280, y: 0 }
data:
rows:
- id: field-1
name: received
value: '{{ trigger.body.orderId }}'
valueMode: expression
keepOnlySetFields: true
edges:
- { id: order-received-to-keep, source: order-received, target: keep }trigger_form
The Form trigger (node page) renders a public form and starts a run when it is submitted.
- Expressions: none. Later nodes read each answer as
{{ trigger.body.<name> }}. - Handles: one output.
- In a Package:
fieldsandauthModetravel.basicAuthUsernameis never carried, and the password is never on the node: withauthMode: basic, the username is collected at install, in the wizard or as--form-user.<node id>=…. - Refusals: a second Form trigger in the workflow (
PORTABLE_DEFINITION_INVALID).
Fields (from FormTriggerNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
fields | list of objects | no | [] | — | as written | The form's questions, in order. |
fields[].name | string | yes | — | — | as written | — |
fields[].type | string | yes | — | text, textarea, number, email, password, date, dropdown, radio, checkboxes, file, hidden | as written | — |
fields[].required | boolean | yes | — | — | as written | — |
fields[].label | string | yes | — | — | as written | — |
fields[].options | list of strings | no | — | — | as written | — |
fields[].acceptedExtensions | list of strings | no | — | — | as written | — |
fields[].allowMultiple | boolean | no | — | — | as written | — |
authMode | string | no | 'none' | none, basic, elizon_login | as written | Who can open and submit the form — see The three auth modes. |
basicAuthUsername | string | no | — | — | never carried | The username for basic. The password is set in the panel, hashed at publish, and never stored in the graph. |
Output
A trigger has no upstream output: later nodes read what started the run as {{ trigger.… }} and {{ run.trigger.… }}. The node page lists the paths.
Example (npx @elizon/cli example workflow --node trigger_form prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Form trigger example
description: Takes a service request from a public form.
graph:
nodes:
- id: request-form
type: trigger_form
position: { x: 0, y: 0 }
data:
authMode: none
fields:
- name: email
label: Your email
type: email
required: true
- name: machine
label: Machine
type: dropdown
required: true
options:
- X200
- X300
- id: keep
type: edit_fields
position: { x: 280, y: 0 }
data:
rows:
- id: field-1
name: received
value: '{{ trigger.body.machine }}'
valueMode: expression
keepOnlySetFields: true
edges:
- { id: request-form-to-keep, source: request-form, target: keep }trigger_email
The Email trigger (node page) polls an IMAP mailbox every pollMinutes and starts one run per message that passes the filters.
- Expressions: none. Later nodes read the message flat:
{{ trigger.subject }},{{ trigger.from.address }},{{ trigger.attachments }}. - Handles: one output.
- In a Package: what to read travels (the filters, the attachment rules,
format,postAction, the poll settings). The mailbox and its server never travel:host,port,security,username,password,mailboxandmoveToMailboxare set on the canvas after install. - Refusals: a second Email trigger in the workflow (
PORTABLE_DEFINITION_INVALID).
Fields (from EmailTriggerNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
host | string | no | '' | — | never carried | The IMAP server. |
port | integer | no | 993 | 1–65535 | never carried | 993 is IMAPS; STARTTLS and plain connections normally use 143. |
security | string | no | 'tls' | tls, starttls, none | never carried | See Connecting a mailbox. |
username | string | no | '' | — | never carried | The account's login. |
password | string | no | '' | — | never carried | Only a secret reference. A typed password is refused by the save. |
mailbox | string | no | 'INBOX' | — | never carried | The folder to watch, exactly as the server names it. |
postAction | string | no | 'markRead' | markRead, move, none | as written | What to do to the message after its run starts — see After processing. |
moveToMailbox | string | no | '' | — | never carried | The folder for move. It must already exist. |
filterUnseen | boolean | no | true | — | as written | Only unread mail. |
filterFrom | string | no | '' | — | as written | Sender address contains. |
filterTo | string | no | '' | — | as written | Recipient address contains. |
filterSubjectContains | string | no | '' | — | as written | Subject contains. |
filterSince | string | no | '' | — | as written | Arrived on or after this day. |
downloadAttachments | boolean | no | false | — | as written | Bring attachments into the run as files. |
attachmentExtensions | string | no | '' | — | as written | Comma-separated extensions to keep (pdf,xlsx). Empty keeps every type. |
attachmentMaxBytes | integer | no | 10485760 | 1–26214400 | as written | Larger attachments are skipped; the email still runs. |
format | string | no | 'simple' | simple, raw | as written | raw also adds {{ trigger.raw }}, the whole message source. |
pollMinutes | integer | no | 5 | 1–60 | as written | How often to check. |
processExistingUnread | boolean | no | false | — | as written | On the first check, also start runs for mail already in the folder. |
processExistingMax | integer | no | 10 | 1–200 | as written | How many of the most recent existing emails to take when processExistingUnread is on. |
Output
A trigger has no upstream output: later nodes read what started the run as {{ trigger.… }} and {{ run.trigger.… }}. The node page lists the paths.
Example (npx @elizon/cli example workflow --node trigger_email prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Email trigger example
description: Reads supplier invoices that arrive in a mailbox.
graph:
nodes:
- id: invoice-mail
type: trigger_email
position: { x: 0, y: 0 }
data:
postAction: markRead
filterUnseen: true
filterSubjectContains: Invoice
downloadAttachments: true
attachmentExtensions: pdf
format: simple
pollMinutes: 5
processExistingUnread: false
- id: keep
type: edit_fields
position: { x: 280, y: 0 }
data:
rows:
- id: field-1
name: received
value: '{{ trigger.subject }}'
valueMode: expression
keepOnlySetFields: true
edges:
- { id: invoice-mail-to-keep, source: invoice-mail, target: keep }trigger_knowledge_document
The Knowledge Base document trigger (node page) starts a run for every document indexed into a Knowledge Base.
- Expressions: none. Later nodes read the document flat, for example
{{ trigger.document_name }}. - Handles: one output.
- In a Package: write
knowledgeBase: knowledgeBases.<name>, a Knowledge Base the Package declares; the installed trigger watches the one the install creates.knowledgeBaseNameis a label and is replaced by that Knowledge Base's name. Declareknowledgeincompatibility.requiredCapabilities. - Refusals:
knowledgeBasemissing ornull(PORTABLE_DEFINITION_INVALID), or naming an undeclared Knowledge Base (UNRESOLVED_LOCAL_REFERENCE): a trigger that watches nothing could never fire.
Fields (from KnowledgeDocumentTriggerNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
knowledgeBase (stored knowledgeBaseId) | string | no | '' | — | as knowledgeBase: knowledgeBases.<name> | The knowledge base to watch, from this workflow's project. See Choosing the knowledge base. |
knowledgeBaseName | string | no | '' | — | as written | Its name, kept for display so the node still says what it watched if the knowledge base is later deleted. |
event | string | no | 'indexed' | indexed | as written | The one event today: a document has finished indexing. |
Output
A trigger has no upstream output: later nodes read what started the run as {{ trigger.… }} and {{ run.trigger.… }}. The node page lists the paths.
Example (npx @elizon/cli example workflow --node trigger_knowledge_document prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Knowledge Base document trigger example
description: Runs once for every document indexed into the handbook.
graph:
nodes:
- id: new-document
type: trigger_knowledge_document
position: { x: 0, y: 0 }
data:
knowledgeBase: knowledgeBases.handbook
knowledgeBaseName: Handbook
event: indexed
- id: keep
type: edit_fields
position: { x: 280, y: 0 }
data:
rows:
- id: field-1
name: received
value: '{{ trigger.document_name }}'
valueMode: expression
keepOnlySetFields: true
edges:
- { id: new-document-to-keep, source: new-document, target: keep }trigger_app
The App trigger (node page) polls an app every pollMinutes, for example new files in a Google Drive folder, and starts one run per new item.
- Expressions:
inputsentries take avalueMode. Later nodes read the item as{{ trigger.item.<field> }}. - Handles: one output.
- In a Package:
app,trigger,inputsand the poll settings travel. A fixed value ininputs(the example'sfolderId) travels as written, so every install polls that same id until the operator changes it in Studio; say so in the Package's description. The account travels asconnection: the id of acredentialSlot with the app's account (app: google), which the operator binds to one of their connected accounts at install. Add the app's provider tocompliance.providers. - Refusals:
connectionnaming an undeclared Slot, a Slot that is nottype: credential, or one whoseappis not the account the app needs (PORTABLE_UNDECLARED_REFERENCE); a second App trigger (PORTABLE_DEFINITION_INVALID). An undeclared provider is a warning (MANIFEST_APP_PROVIDER_UNDECLARED).
Fields (from AppTriggerNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
app | string | no | '' | — | as written | The app, for example google-drive. |
trigger | string | no | '' | — | as written | Which of that app's triggers, for example file-added. Each app's page lists them. |
connection (stored connectionId) | string | no | '' | — | as connection: a credential Slot id, or null | The connected account to watch, from Connections. |
inputs | map of name → object | no | {} | — | as written | The trigger's own settings — the folder, the calendar, the sheet range. Each app's page lists them. |
inputs.<name>.value | string | no | '' | — | as written | — |
inputs.<name>.valueMode | string | no | 'fixed' | expression, fixed | as written | — |
pollMinutes | integer | no | 5 | 1–60 | as written | How often to check. |
processExisting | boolean | no | false | — | as written | On the first check, also start runs for items that already exist. |
processExistingMax | integer | no | 10 | 1–200 | as written | How many existing items to take when processExisting is on. |
Output
A trigger has no upstream output: later nodes read what started the run as {{ trigger.… }} and {{ run.trigger.… }}. The node page lists the paths.
Example (npx @elizon/cli example workflow --node trigger_app prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: App trigger example
description: Runs once for every file dropped into a Google Drive folder.
graph:
nodes:
- id: new-file
type: trigger_app
position: { x: 0, y: 0 }
data:
app: google-drive
trigger: file-added
connection: google-account
inputs:
folderId:
value: intake-folder-id
valueMode: fixed
pollMinutes: 5
processExisting: false
processExistingMax: 10
- id: keep
type: edit_fields
position: { x: 280, y: 0 }
data:
rows:
- id: field-1
name: received
value: '{{ trigger.item.name }}'
valueMode: expression
keepOnlySetFields: true
edges:
- { id: new-file-to-keep, source: new-file, target: keep }AI
ai_agent
The AI Agent node (node page) runs one of the Package's Agents.
- Expressions: each
inputMappings[].expression. - Handles: one output; the answer is
{{ upstream.<node id>.output.text }}. - In a Package: write
mode: existingandagent: agents.<name>(ornullfor none chosen yet, whichvalidateandbuildwarn about withWORKFLOW_NODE_UNBOUND: the node installs but cannot run); the storedagentIdis never in the file.agentNameis a label and is replaced by the installed Agent's name. Theinlinemode (instructions, model and tools on the node itself) is still decoded, withllm_slotfor its model,tools[].tool: tools.<name>(MCP servers only) andknowledgeSources[].knowledgeBase: knowledgeBases.<name>, but Studio no longer saves new inline nodes: use an Agent. - Refusals:
agentnaming an undeclared Agent (UNRESOLVED_LOCAL_REFERENCE).
Fields (from AiAgentNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
mode | string | yes | — | existing, inline | as written | Always existing: the node runs a saved agent. Agents built inline on the node are no longer supported and are refused when the workflow is saved. |
agent (stored agentId) | string | yes | — | — | as agent: agents.<name>, or null | With mode: existing. The agent to run. It must belong to the workflow's organization. |
agentName | string | no | — | — | as written | With mode: existing. The agent's name, kept for display. |
inputMappings | list of objects | yes | — | — | as written | With mode: existing. What to send the agent. Each expression is resolved with the {{ }} syntax. One mapping sends its value; several send a JSON object keyed by field. |
inputMappings[].field | string | yes | — | — | as written | With mode: existing. |
inputMappings[].expression | string | yes | — | — | as written | With mode: existing. |
instructions | string | yes | — | — | as written | With mode: inline. |
llm_slot (stored llm_config_id) | string or null | no | — | — | as llm_slot: an llm Slot id | With mode: inline. |
model_id | string or null | no | — | — | as written | With mode: inline. |
tools | list of objects | yes | — | — | converted per entry (see notes) | With mode: inline. |
tools[].type | string | yes | — | mcp_server, connector | converted per entry (see notes) | With mode: inline. |
tools[].id | string | yes | — | — | converted per entry (see notes) | With mode: inline. |
tools[].name | string | yes | — | — | converted per entry (see notes) | With mode: inline. |
knowledgeSources | list of objects | yes | — | — | converted per entry (see notes) | With mode: inline. |
knowledgeSources[].type | string | yes | — | knowledge_base, url | converted per entry (see notes) | With mode: inline. |
knowledgeSources[].id | string | no | — | — | converted per entry (see notes) | With mode: inline. |
knowledgeSources[].url | string | no | — | — | converted per entry (see notes) | With mode: inline. |
knowledgeSources[].label | string | yes | — | — | converted per entry (see notes) | With mode: inline. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
text | string | Response text |
json | object|null | Structured JSON (if the agent emitted any) |
model | string | Model used |
toolCalls | array | Tool calls |
knowledgeQueriesCount | number | Knowledge queries count |
tokenUsage.prompt | number | Prompt tokens |
tokenUsage.completion | number | Completion tokens |
estimatedCostUsd | number | Estimated cost (USD) |
latencyMs | number | Latency (ms) |
Example (npx @elizon/cli example workflow --node ai_agent prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: AI Agent example
description: Has the Package’s Agent answer a request.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: answer
type: ai_agent
position: { x: 280, y: 0 }
data:
mode: existing
agent: agents.responder
agentName: Responder
inputMappings:
- field: request
expression: '{{ trigger.request }}'
edges:
- { id: start-to-answer, source: start, target: answer }team_step
The Team step (node page) hands its input to one of the Package's Teams.
- Expressions:
inputExpression. - Handles: one output; the Team's answer is
{{ upstream.<node id>.output.text }}. - In a Package: write
team: teams.<name>(ornull, whichvalidateandbuildwarn about withWORKFLOW_NODE_UNBOUND: the node installs but cannot run);teamIdis never in the file.teamNameis replaced by the installed Team's name. Declareteamsincompatibility.requiredCapabilities. - Refusals:
teamnaming an undeclared Team (UNRESOLVED_LOCAL_REFERENCE).
Fields (from TeamStepNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
team (stored teamId) | string | yes | — | — | as team: teams.<name>, or null | The team to run. It must belong to the workflow's organization. |
teamName | string | no | — | — | as written | The team's name, kept for display. |
inputExpression | string | no | '' | — | as written | What to send the team, using the {{ }} syntax. Empty forwards what reached the node, the same way the AI Agent node does. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
text | string | Response text |
teamId | string | Team id |
runId | string|null | Run id |
sessionId | string|null | Session id |
tokenUsage.prompt | number | Prompt tokens |
tokenUsage.completion | number | Completion tokens |
latencyMs | number | Latency (ms) |
Example (npx @elizon/cli example workflow --node team_step prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Team step example
description: Hands a request to the Package’s Team.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: review
type: team_step
position: { x: 280, y: 0 }
data:
team: teams.triage
teamName: Triage
inputExpression: '{{ trigger.request }}'
edges:
- { id: start-to-review, source: start, target: review }ai_prompt
The AI Prompt node (node page) sends one prompt to a model.
- Expressions:
instructions,inputExpression,attachmentExpression. - Handles: one output; the text is
{{ upstream.<node id>.output.completionText }}. - In a Package: write
llm_slot: <slot id>, anllmSlot the manifest declares; the operator binds it to one of their LLM Configurations at install.model_idis a preference, used when that Configuration offers the model. Everything else travels. - Refusals: a value Studio could not save (
PORTABLE_DEFINITION_INVALID). Anllm_slotthat names no declared Slot is not caught byvalidateorbuildtoday; install refuses it (PORTABLE_UNRESOLVED_BINDING). Check the Slot id yourself.
Fields (from AiPromptNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
llm_slot (stored llm_config_id) | string or null | no | — | — | as llm_slot: an llm Slot id | The LLM provider connection to use, from Settings → LLM Configurations. |
model_id | string or null | no | — | — | as written | The model on that connection. |
instructions | string | no | '' | — | as written | The system prompt. May contain {{ }} references. Studio keeps a version history of it; the active version is what runs. |
inputExpression | string | no | '' | — | as written | The user message, with {{ }} references. Empty forwards what reached the node, the same way the AI Agent node does. |
attachmentExpression | string | no | '' | — | as written | One uploaded file for the model, for example {{ trigger.body.scan }} from a Form file field or {{ upstream.download.output.file }} from an FTP download. An image or PDF is sent as the file itself, and the model must be marked vision-capable. Any other file (Word, Excel, CSV, text) is sent as its text, after the input, and works with any model. |
temperature | number | no | 0.7 | 0–2 | as written | Sampling temperature. Lower is more repeatable. |
maxTokens | number | no | 4096 | 1–128000 | as written | The answer's token limit. |
outputFormat | string | no | 'text' | text, structured, custom | as written | Text passes the answer through. Structured asks for, and checks, one of three fixed JSON shapes. Custom asks for, and checks, your own JSON Schema. |
structuredSchemaId | string | no | — | — | as written | The fixed shape for Structured: classification is { category, confidence, summary }, sentiment is { sentiment, confidence }, extraction is { fields: { … } }. |
customJsonSchema | string | no | — | — | as written | A JSON Schema for Custom. It may itself be a {{ }} reference, so a caller can supply the schema per run. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
completionText | string | Completion text |
structuredOutput | unknown | Structured output (only when Output format is Structured/Custom) |
modelUsed | string | Model used |
tokenUsage.prompt | number | Prompt tokens |
tokenUsage.completion | number | Completion tokens |
latencyMs | number | Latency (ms) |
Example (npx @elizon/cli example workflow --node ai_prompt prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: AI Prompt example
description: Summarises a request in two sentences.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: summarise
type: ai_prompt
position: { x: 280, y: 0 }
data:
llm_slot: reasoning
model_id: null
instructions: Summarise the request in two sentences.
inputExpression: '{{ trigger.request }}'
temperature: 0.2
maxTokens: 512
outputFormat: text
edges:
- { id: start-to-summarise, source: start, target: summarise }classify
The Classify node (node page) asks a model which of its categories the input belongs to, and routes it down that category's handle.
- Expressions:
inputPrompt. - Handles:
category-<id>per category, pluscategory-otherwithnoMatchAction: other_branch. WithallowMultipleClasses: trueseveral handles can fire; withnoMatchAction: discardan unmatched input fires none. - In a Package:
llm_slotas for AI Prompt. Categoryids are local to the graph: Studio writes UUIDs, and a short id such asbillingworks the same and reads better in a file. - Refusals: as for AI Prompt.
Fields (from ClassifyNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
llm_slot (stored llm_config_id) | string or null | no | — | — | as llm_slot: an llm Slot id | The LLM provider connection to use. |
model_id | string or null | no | — | — | as written | The model on that connection. |
inputPrompt | string | no | '{{ trigger.body.text }}' | — | as written | The text to classify, with {{ }} references. Empty forwards what reached the node. |
categories | list of objects | yes | — | — | as written | At least one. name is what the model picks between and description tells it when to pick it. id is stable, so renaming a category keeps its branch wired. |
categories[].id | string | yes | — | — | as written | — |
categories[].name | string | yes | — | — | as written | — |
categories[].description | string | yes | — | — | as written | — |
allowMultipleClasses | boolean | no | false | — | as written | Let the model pick several categories. Every matching branch then runs. |
noMatchAction | string | no | 'discard' | discard, other_branch | as written | When nothing matches: discard stops this branch of the run; other_branch continues down the node's Other handle. |
systemPromptTemplate | string | no | — | — | as written | Replace the platform's classification prompt. Empty uses the default. |
enableAutoFixing | boolean | no | false | — | as written | When the model's answer is not exactly one of your category names, match it case-insensitively, then as a substring, before treating it as no match. No second model call is made. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
inputText | string | Input text |
assignedCategories | string[] | Assigned categories |
discarded | boolean | Discarded (no match) |
modelUsed | string | Model used |
tokenUsage.prompt | number | Prompt tokens |
tokenUsage.completion | number | Completion tokens |
latencyMs | number | Latency (ms) |
autoFixApplied | boolean | Auto-fix applied |
Example (npx @elizon/cli example workflow --node classify prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Classify example
description: Sends each request down the branch of its category.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: route
type: classify
position: { x: 280, y: 0 }
data:
llm_slot: reasoning
model_id: null
inputPrompt: '{{ trigger.request }}'
categories:
- id: billing
name: Billing
description: Invoices, payments, refunds.
- id: technical
name: Technical
description: Faults, errors, how-to questions.
allowMultipleClasses: false
noMatchAction: other_branch
enableAutoFixing: false
- id: billing-queue
type: edit_fields
position: { x: 560, y: 0 }
data:
rows:
- id: field-1
name: queue
value: billing
valueMode: fixed
keepOnlySetFields: true
- id: technical-queue
type: edit_fields
position: { x: 840, y: 160 }
data:
rows:
- id: field-1
name: queue
value: technical
valueMode: fixed
keepOnlySetFields: true
- id: other-queue
type: edit_fields
position: { x: 1120, y: 320 }
data:
rows:
- id: field-1
name: queue
value: general
valueMode: fixed
keepOnlySetFields: true
edges:
- { id: start-to-route, source: start, target: route }
- {
id: route-category-billing-to-billing-queue,
source: route,
target: billing-queue,
sourceHandle: category-billing,
}
- {
id: route-category-technical-to-technical-queue,
source: route,
target: technical-queue,
sourceHandle: category-technical,
}
- {
id: route-category-other-to-other-queue,
source: route,
target: other-queue,
sourceHandle: category-other,
}Control flow
if_else
The If/Else node (node page) checks its branches top to bottom and takes the first whose expression is true, or defaultBranchId.
- Expressions: each
branches[].expression, for example'{{ trigger.body.total }} > 10000'. - Handles: each branch
id, anddefaultBranchId. Exactly one is taken; the nodes after the others are skipped. - In a Package: everything travels. Branch
ids are local to the graph: Studio writes UUIDs (andbranch-1for the first), and a short id such aslargeworks the same. - Refusals: only the ones every node has.
Fields (from IfElseNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
branches | list of objects | no | [] | at most 5 entries | as written | The conditions, tested top to bottom. id is the branch's output handle; label is what the canvas shows. |
branches[].id | string | yes | — | — | as written | — |
branches[].label | string | yes | — | — | as written | — |
branches[].expression | string | yes | — | — | as written | — |
defaultBranchId | string | no | 'default' | — | as written | The handle taken when no condition holds — the Else branch. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
branchTaken | string | Branch id taken |
Example (npx @elizon/cli example workflow --node if_else prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: If/Else example
description: Sends large orders one way and the rest another.
graph:
nodes:
- id: order
type: trigger_http
position: { x: 0, y: 0 }
data:
method: POST
fields:
- name: total
type: number
required: true
- id: check-total
type: if_else
position: { x: 280, y: 0 }
data:
branches:
- id: large
label: Over 10 000
expression: '{{ trigger.body.total }} > 10000'
defaultBranchId: default
- id: needs-approval
type: edit_fields
position: { x: 560, y: 0 }
data:
rows:
- id: field-1
name: status
value: needs approval
valueMode: fixed
keepOnlySetFields: true
- id: accepted
type: edit_fields
position: { x: 840, y: 160 }
data:
rows:
- id: field-1
name: status
value: accepted
valueMode: fixed
keepOnlySetFields: true
edges:
- { id: order-to-check-total, source: order, target: check-total }
- {
id: check-total-large-to-needs-approval,
source: check-total,
target: needs-approval,
sourceHandle: large,
}
- {
id: check-total-default-to-accepted,
source: check-total,
target: accepted,
sourceHandle: default,
}loop
The Loop node (node page) runs its body once per item of sourceArray, with up to batchSize iterations at the same time (1, the default, runs them one after another).
- Expressions:
sourceArray. Inside the body,{{ loop.item }}and{{ loop.index }}. - Handles:
loop(the body) anddone(runs once, after the last item). After the loop, read a body node's results as{{ upstream.<body node id>.output.iterations }}, a list with one entry per iteration (outputitself is{ iterations, loopNodeId }). - In a Package: everything travels.
- Refusals: only the ones every node has. A Wait node inside a loop body fails the loop at run time.
Fields (from LoopNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
sourceArray | string | no | '' | — | as written | A {{ }} expression that resolves to a list, for example {{ trigger.body.orders }} or {{ upstream.search.output.rows }}. |
batchSize | integer | no | 1 | ≥ 1 | as written | How many items run at the same time (1 = one after another). Each iteration still gets one item. |
Output
Read as {{ upstream.<node id>.output }} (an array).
Note: Reference this Loop’s BODY NODES directly instead — after the loop, {{ upstream.<body node id>.output.iterations }} is an array of that node’s own output, one entry per iteration, which is what you almost always want. The body node’s output holds that array under iterations, beside loopNodeId.
Example (npx @elizon/cli example workflow --node loop prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Loop example
description: Runs one step per order line, then one step when every line is done.
graph:
nodes:
- id: order
type: trigger_http
position: { x: 0, y: 0 }
data:
method: POST
fields:
- name: lines
type: object
required: true
- id: each-line
type: loop
position: { x: 280, y: 0 }
data:
sourceArray: '{{ trigger.body.lines }}'
batchSize: 1
- id: line-total
type: edit_fields
position: { x: 560, y: 0 }
data:
rows:
- id: field-1
name: total
value: '{{ loop.item.quantity }} x {{ loop.item.price }}'
valueMode: expression
keepOnlySetFields: true
- id: collect
type: edit_fields
position: { x: 840, y: 160 }
data:
rows:
- id: field-1
name: totals
value: '{{ upstream.line-total.output }}'
valueMode: expression
keepOnlySetFields: true
edges:
- { id: order-to-each-line, source: order, target: each-line }
- {
id: each-line-loop-to-line-total,
source: each-line,
target: line-total,
sourceHandle: loop,
}
- { id: each-line-done-to-collect, source: each-line, target: collect, sourceHandle: done }wait
The Wait node (node page) pauses the run for amount unit (interval), or until at in timezone (until).
- Expressions:
at. - Handles: one output.
- In a Package: everything travels.
- Refusals: only the ones every node has.
Fields (from WaitNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
mode | string | no | 'interval' | interval, until | as written | After time interval or At specified time. |
amount | integer | no | 1 | ≥ 1 | as written | How long, in unit. Interval mode. |
unit | string | no | 'hours' | seconds, minutes, hours, days | as written | Interval mode. |
at | string | no | '' | — | as written | The date and time to resume at, or a {{ }} expression that resolves to one. Until mode. |
timezone | string | no | 'UTC' | — | as written | The zone at is read in. Ignored when at carries its own offset or Z. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
mode | string | Mode ("interval" | "until") |
resumeAt | string | When the run resumed (ISO 8601, UTC) |
timezone | string | The node's time zone |
waitedMs | number | How long the run waited (ms) |
alreadyPassed | boolean | Target time had already passed |
message | string | What the node did, in words |
Example (npx @elizon/cli example workflow --node wait prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Wait example
description: Waits three days before the next step.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: three-days
type: wait
position: { x: 280, y: 0 }
data:
mode: interval
amount: 3
unit: days
at: ''
timezone: Europe/Berlin
- id: follow-up
type: edit_fields
position: { x: 560, y: 0 }
data:
rows:
- id: field-1
name: due
value: '{{ run.startedAt }}'
valueMode: expression
keepOnlySetFields: true
edges:
- { id: start-to-three-days, source: start, target: three-days }
- { id: three-days-to-follow-up, source: three-days, target: follow-up }human_review
The Human Review node (node page) pauses the run until a reviewer answers, or until the timeout. In single mode the reviewer answers once (responseFields); in batch mode they approve, edit or reject each item of itemsExpression (itemFields).
- Expressions:
title,message,itemsExpression,itemLabel(with{{ item.<field> }}), and each fielddefaultwithvalueMode: expression.assignedTotoo: see below. - Handles: one output. Branch on
{{ upstream.<node id>.output.decision }}(approvedorrejected) with an If/Else node; a batch review'sapprovedlist feeds a Loop. - In a Package: the file carries
approverRequired. LeaveassignedToout and setapproverRequired: true, and the approvers are collected at install (in the wizard, or--approver.<node id>=…). Or setassignedToto{{ }}expressions only (for example'{{ trigger.body.managerEmail }}') andapproverRequired: false: it travels, and the installer's answer becomes the fallback.fallbackAssignedTois never carried. - Refusals: a literal address in
assignedTois stripped bybuild, with the warningHUMAN_REVIEW_APPROVER_STRIPPED(fromvalidatetoo).
Fields (from HumanReviewNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
title | string | no | '' | — | as written | What the reviewer sees as the request's title. May contain {{ }} references. |
message | string | no | '' | — | as written | The request's body. May contain {{ }} references. |
assignedTo | string | no | '' | — | only when {{ }} expressions only; otherwise stripped | Who reviews: email addresses, {{ }} expressions, or a mix, comma-separated — for example {{ upstream.lookup.output.result.accountOwnerEmail }}. Resolved when the step runs (see below). |
fallbackAssignedTo | string | no | '' | — | never carried | Literal email addresses used when none of the resolved approvers can review. |
responseFields | list of objects | no | [] | — | as written | What the reviewer sees and can edit — each text, long_text, html, yes_no, email, number, or date. default is { value, valueMode: fixed | expression }, pre-filled for the reviewer. |
responseFields[].name | string | yes | — | — | as written | — |
responseFields[].type | string | yes | — | text, long_text, html, yes_no, email, number, date | as written | — |
responseFields[].default | object | no | — | — | as written | — |
responseFields[].default.value | string | yes | — | — | as written | — |
responseFields[].default.valueMode | string | yes | — | fixed, expression | as written | — |
timeoutValue | integer | no | 24 | ≥ 1 | as written | How long to wait, in timeoutUnit. Between 1 hour and 30 days in total. |
timeoutUnit | string | no | 'hours' | hours, days | as written | The unit of timeoutValue. |
onTimeout | string | no | 'expire' | expire, approve, reject | as written | What happens when nobody answers in time (see Timeouts). |
mode | string | no | 'single' | single, batch | as written | batch reviews a list of items in one request — see Batch review. |
itemsExpression | string | no | '' | — | as written | Batch only. An expression that resolves to a list of objects, e.g. {{ upstream.drafts.output.rows }}. |
itemLabel | string | no | '' | — | as written | Batch only. Each item's title, an expression over item, e.g. {{ item.customerName }} — {{ item.invoiceNumber }}. Blank shows "Item 1", "Item 2", … |
itemFields | list of objects | no | [] | — | as written | Batch only. The editable fields per item, with the same types as responseFields. A default may read the item: {{ item.draftBody }}. |
itemFields[].name | string | yes | — | — | as written | — |
itemFields[].type | string | yes | — | text, long_text, html, yes_no, email, number, date | as written | — |
itemFields[].default | object | no | — | — | as written | — |
itemFields[].default.value | string | yes | — | — | as written | — |
itemFields[].default.valueMode | string | yes | — | fixed, expression | as written | — |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
outcome | string | Outcome ("responded" | "timed_out") |
decision | string | Decision ("approved" | "rejected") |
resolvedBy | string | Resolved by ("reviewer" | "timeout") |
requestId | string | Request id |
timeoutAt | string | Timeout deadline |
responderEmail | string|null | Responder email (responded only) |
respondedAt | string|null | Responded at (responded only) |
droppedApprovers | string[] | Approvers dropped (not a member with review rights) |
More paths follow from the node’s own configuration; the node page lists them.
Example (npx @elizon/cli example workflow --node human_review prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Human Review example
description: Asks finance to approve a refund before anything else happens.
graph:
nodes:
- id: refund-request
type: trigger_http
position: { x: 0, y: 0 }
data:
method: POST
fields:
- name: amount
type: number
required: true
- id: approve-refund
type: human_review
position: { x: 280, y: 0 }
data:
title: Refund of {{ trigger.body.amount }}
message: Approve or reject this refund.
mode: single
responseFields:
- name: notes
type: long_text
default:
value: Looks fine.
valueMode: fixed
timeoutValue: 2
timeoutUnit: days
onTimeout: reject
approverRequired: true
edges:
- { id: refund-request-to-approve-refund, source: refund-request, target: approve-refund }Data and files
variable
The Variable node (node page) sets a run-scoped variable that every later node reads as {{ variables.<name> }}.
- Expressions:
initialValue. - Handles: one output.
- In a Package: everything travels.
- Refusals: only the ones every node has.
Fields (from VariableNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
name | string | no | '' | — | as written | The variable's name, read as {{ variables.<name> }}. |
type | string | no | 'string' | string, number, boolean, object | as written | What the value is converted to. |
initialValue | string | no | '' | — | as written | The value, as text or a {{ }} expression. It is converted to type: "42" becomes the number 42, "true" the boolean, JSON text an object. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
value | declared | Value (coerced to the declared type) |
Note: Prefer {{ variables.<name> }} — this node also declares a run-scoped variable.
Example (npx @elizon/cli example workflow --node variable prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Variable example
description: Stores whether a ticket is urgent, for every later step to read.
graph:
nodes:
- id: ticket
type: trigger_http
position: { x: 0, y: 0 }
data:
method: POST
fields:
- name: urgent
type: boolean
required: true
- id: priority
type: variable
position: { x: 280, y: 0 }
data:
name: urgent
type: boolean
initialValue: '{{ trigger.body.urgent }}'
edges:
- { id: ticket-to-priority, source: ticket, target: priority }edit_fields
The Edit Fields node (node page) builds an object from rows, one field per row.
- Expressions: each
rows[].valuewithvalueMode: expression. - Handles: one output; each row is
{{ upstream.<node id>.output.<name> }}. - In a Package: everything travels.
- Refusals: only the ones every node has.
Fields (from EditFieldsNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
rows | list of objects | no | [] | — | as written | One field each. valueMode is expression (the default: value is resolved) or fixed (value is used as written). |
rows[].id | string | yes | — | — | as written | — |
rows[].name | string | yes | — | — | as written | — |
rows[].value | string | yes | — | — | as written | — |
rows[].valueMode | string | no | 'expression' | expression, fixed | as written | — |
keepOnlySetFields | boolean | no | true | — | as written | The output holds exactly the rows you set. Must stay on — see below. |
Output
Read as {{ upstream.<node id>.output }} (an object).
More paths follow from the node’s own configuration; the node page lists them.
Example (npx @elizon/cli example workflow --node edit_fields prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Edit Fields example
description: Maps a lead form onto the field names a CRM expects.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: crm-fields
type: edit_fields
position: { x: 280, y: 0 }
data:
rows:
- id: r1
name: contact_name
value: '{{ trigger.name }}'
valueMode: expression
- id: r2
name: source
value: website
valueMode: fixed
keepOnlySetFields: true
edges:
- { id: start-to-crm-fields, source: start, target: crm-fields }convert_to_file
The Convert to File node (node page) turns a value into a stored file (format), which Send Email, AI Prompt, Code and Data Table nodes can use.
- Expressions:
sourceExpression,filename. - Handles: one output; the file is
{{ upstream.<node id>.output.file }}. - In a Package: everything travels.
- Refusals: only the ones every node has.
Fields (from ConvertToFileNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
sourceExpression | string | no | '' | — | as written | What to write, usually an expression such as {{ upstream.report-writer.output.text }}. |
format | string | no | 'md' | md, txt, json, csv, html, pdf, docx, xlsx | as written | The file format. |
filename | string | no | 'report' | — | as written | Expressions allowed. The format's extension is added when the name lacks it. |
shareWith | string | no | 'workflow' | workflow, project | as written | This workflow only, or Every workflow in this project — see below. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
file | file | File (attach it, or read it with AI Prompt) |
file.filename | string | File name |
file.contentType | string | Content type |
file.sizeBytes | number | Size (bytes) |
file.text | string | Text content |
Example (npx @elizon/cli example workflow --node convert_to_file prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Convert to File example
description: Turns a Markdown report into a PDF the run can attach.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: to-pdf
type: convert_to_file
position: { x: 280, y: 0 }
data:
sourceExpression: |-
# Weekly report
{{ trigger.summary }}
format: pdf
filename: weekly-report
shareWith: workflow
edges:
- { id: start-to-to-pdf, source: start, target: to-pdf }data_table
The Data Table node (node page) runs one resource / operation against a Data Table: row insert, get, update, upsert, bulkUpsert, delete, exists, or a table operation.
- Expressions:
mappingRows[].value,conditions[].valueandautomaticSource.valuewithvalueMode: expression;rowsFileExpression;cursor. - Handles: one output. Branch on
{{ upstream.<node id>.output.result.exists }}or.affected. - In a Package: write
table: dataTables.<name>, a Data Table the Package declares (ornull);tableIdis never in the file.tableNameis a label and is replaced by the installed table's name, except on a tablecreate, where it is the name created. Anupdate,upsertordeleteneeds a key: aconditionsentry naming a column (bulkUpsertkeys onkeyColumnsinstead). - Refusals:
tablenaming an undeclared Data Table (UNRESOLVED_LOCAL_REFERENCE). A missing key is a warning (DATA_TABLE_NODE_KEY_MISSING): the platform refuses to publish or run the node until it has one.
Fields (from DataTableNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
resource | string | no | 'row' | table, row | as written | What the operation acts on. See Operations at a glance. |
operation | string | no | 'insert' | create, list, rename, delete, addColumn, removeColumn, insert, get, update, upsert, bulkUpsert, exists | as written | The operation, for example insert, get, update, upsert, bulkUpsert, delete, exists, create, list, rename, addColumn, removeColumn. |
table (stored tableId) | string | no | '' | — | as table: dataTables.<name>, or null | The table, from this workflow's project. See Choosing the table. |
tableName | string | no | '' | — | as written | The table's name — kept for display, and the name for Create table. |
columns | list of objects | no | [] | — | as written | Create table: the columns, each a name and a type. |
columns[].id | string | no | — | — | as written | — |
columns[].name | string | no | '' | — | as written | — |
columns[].type | string | no | 'string' | boolean, date, number, string | as written | — |
returnExistingIfExists | boolean | no | false | — | as written | Create table: return the table if one by that name exists, instead of failing. |
newTableName | string | no | '' | — | as written | Rename table: the new name. |
columnName | string | no | '' | — | as written | Add / Remove column: the column. |
columnType | string | no | 'string' | boolean, date, number, string | as written | Add column: its type. |
mapping | string | no | 'manual' | manual, automatic | as written | How a write's cells are set. See Writing cells. |
mappingRows | list of objects | no | [] | — | as written | Manual mapping: one row per column. |
mappingRows[].id | string | no | '' | — | as written | — |
mappingRows[].value | string | no | '' | — | as written | — |
mappingRows[].valueMode | string | no | 'expression' | expression, fixed | as written | — |
mappingRows[].column | string | no | '' | — | as written | — |
automaticSource | object | no | {"id":"","value":"","valueMode":"expression"} | — | as written | Automatic mapping: where the values come from. |
automaticSource.id | string | no | '' | — | as written | — |
automaticSource.value | string | no | '' | — | as written | — |
automaticSource.valueMode | string | no | 'expression' | expression, fixed | as written | — |
conditions | list of objects | no | [] | — | as written | Which rows to match. See Conditions. |
conditions[].id | string | no | '' | — | as written | — |
conditions[].value | string | no | '' | — | as written | — |
conditions[].valueMode | string | no | 'expression' | expression, fixed | as written | — |
conditions[].column | string | no | '' | — | as written | — |
conditions[].operator | string | no | 'eq' | eq, neq, gt, gte, lt, lte, isEmpty, isNotEmpty, contains, startsWith | as written | — |
conditionMatch | string | no | 'all' | any, all | as written | Whether every condition or any one must hold. |
orderBy | list of objects | no | [] | — | as written | Sort order for Get and List tables. |
orderBy[].id | string | no | '' | — | as written | — |
orderBy[].field | string | no | '' | — | as written | — |
orderBy[].direction | string | no | 'asc' | asc, desc | as written | — |
limit | integer or null | no | null | ≥ 1 | as written | How many rows or tables to return. |
returnAll | boolean | no | false | — | as written | Get: return every matching row. |
keyColumns | list of strings | no | [] | — | as written | Bulk upsert: the column names a row is matched on. See Bulk upsert by key. |
rowsSourceMode | string | no | 'array' | array, file | as written | Bulk upsert and an automatic insert: whether the rows come from an array expression or a file. |
rowsFileExpression | string | no | '' | — | as written | The file holding the rows (JSON array or JSONL), when rowsSourceMode is file. |
cursor | string | no | '' | — | as written | Get: the nextCursor of a previous page. See Paging with a cursor. |
readOutput | string | no | 'rows' | rows, file | as written | Get: a page of rows, or every matching row as one JSONL file. |
dryRun | boolean | no | false | — | as written | Report what a destructive operation would do, without doing it. |
optimizeBulk | boolean | no | false | — | as written | Insert: write many rows in one statement. |
applyToAllRows | boolean | no | false | — | as written | Update / Delete with no conditions: confirm it is meant for every row. |
allowTableDelete | boolean | no | false | — | as written | Delete table: confirm it. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
resource | string | Resource ("table" | "row") |
operation | string | Operation |
table.id | string | Table id |
table.name | string | Table name |
affected | number | Rows (or tables) the operation affected |
dryRun | boolean | Ran as a Dry Run (wrote nothing) |
More paths follow from the node’s own configuration; the node page lists them.
Note: Branch on {{ upstream.<id>.output.result.exists }} (Rows exist) or {{ upstream.<id>.output.affected }} — both are plain values an If/Else can compare.
Example (npx @elizon/cli example workflow --node data_table prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Data Table example
description: Adds one row per request to the Package’s own table.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: log-request
type: data_table
position: { x: 280, y: 0 }
data:
resource: row
operation: insert
table: dataTables.orders
tableName: Orders
mapping: manual
mappingRows:
- id: map-1
column: Order
value: '{{ trigger.order }}'
valueMode: expression
- id: map-2
column: Status
value: received
valueMode: fixed
edges:
- { id: start-to-log-request, source: start, target: log-request }knowledge_search
The Knowledge Search node (node page) searches a Knowledge Base by query, metadataFilters and documentName, and returns up to maxResults documents.
- Expressions:
query, andmetadataFilters[].valuewithvalueMode: expression. - Handles: one output;
{{ upstream.<node id>.output.documents }}and.count. - In a Package: write
knowledgeBase: knowledgeBases.<name>, a Knowledge Base the Package declares;knowledgeBaseNameis replaced by the installed one's name. A filter'sfieldis one of that Knowledge Base'sindexConfig.metadata.fields. - Refusals:
knowledgeBasemissing ornull(PORTABLE_DEFINITION_INVALID), or naming an undeclared Knowledge Base (UNRESOLVED_LOCAL_REFERENCE).
Fields (from KnowledgeSearchNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
knowledgeBase (stored knowledgeBaseId) | string | no | '' | — | as knowledgeBase: knowledgeBases.<name> | The knowledge base to search, from this workflow's project. |
knowledgeBaseName | string | no | '' | — | as written | Its name, kept for display on the node card. |
query | string | no | '' | — | as written | Optional. Text or an expression. Empty means an exact lookup. |
metadataFilters | list of objects | no | [] | at most 20 entries | as written | Each row compares one of the knowledge base's declared metadata fields with a value, Fixed or an expression. All rows must match (AND). |
metadataFilters[].id | string | no | '' | — | as written | — |
metadataFilters[].field | string | no | '' | — | as written | — |
metadataFilters[].value | string | no | '' | — | as written | — |
metadataFilters[].valueMode | string | no | 'fixed' | expression, fixed | as written | — |
documentName | object | no | — | — | as written | Optional. Matches the document's filename, case-insensitive. |
documentName.operator | string | no | 'contains' | equals, contains | as written | — |
documentName.value | string | no | '' | — | as written | — |
maxResults | integer | no | 10 | 1–50 | as written | How many documents to return, 1–50. |
includeFiles | boolean | no | false | — | as written | Adds each document's original as a file you can attach. See Files. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
documents | object[] | Documents found |
count | number | How many documents |
documents.0.documentName | string | First document’s name |
documents.0.documentId | string | First document’s id |
documents.0.source | object | First document’s source (citation) |
documents.0.metadata | json | First document’s metadata |
documents.0.score | number|null | First document’s score (query only) |
documents.0.excerpt | string|null | First document’s best excerpt (query only) |
documents.0.file | file|null | First document’s file (Include files) |
Note: Attach {{ upstream.<id>.output.documents.0.file }} in Send Email (turn on Include files). Branch on {{ upstream.<id>.output.count }} to handle "nothing found".
Example (npx @elizon/cli example workflow --node knowledge_search prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Knowledge Search example
description: Finds the handbook passages about a question.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: find-passages
type: knowledge_search
position: { x: 280, y: 0 }
data:
knowledgeBase: knowledgeBases.handbook
knowledgeBaseName: Handbook
query: '{{ trigger.question }}'
metadataFilters:
- id: by-team
field: team
value: support
valueMode: fixed
maxResults: 5
includeFiles: false
edges:
- { id: start-to-find-passages, source: start, target: find-passages }Integrations
send_email
The Send Email node (node page) sends a message through an SMTP server.
- Expressions:
to,cc,bcc,replyTo,subject,body, each ofattachments, and the threading idsmessageId,inReplyTo,references. - Handles: one output;
{{ upstream.<node id>.output.messageId }}. - In a Package: the message travels. The server, login and sender never do (
smtpHost,smtpPort,smtpSecurity,smtpUsername,smtpPassword,fromAddress,fromName): they are set on the canvas after install, and the node fails with that reason until they are.to,cc,bccandreplyTotravel only when they are{{ }}expressions only, such as'{{ loop.item.Email }}'. - Refusals: a literal recipient is stripped by
build, with the warningSEND_EMAIL_RECIPIENT_STRIPPED(fromvalidatetoo). An SMTP field in the file is refused (PORTABLE_DEFINITION_INVALID).
Fields (from SendEmailNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
smtpHost | string | no | '' | — | never carried | The SMTP server. |
smtpPort | integer | no | 587 | 1–65535 | never carried | Usually 587 for STARTTLS and 465 for TLS. |
smtpSecurity | string | no | 'starttls' | tls, starttls, none | never carried | none is for a relay on a trusted network only. |
smtpUsername | string | no | '' | — | never carried | Empty for a server that needs no login. |
smtpPassword | string | no | '' | — | never carried | Only a secret reference. |
fromAddress | string | no | '' | — | never carried | The sender address. |
fromName | string | no | '' | — | never carried | The sender's display name. |
to | string | no | '' | — | only when {{ }} expressions only; otherwise stripped | Comma-separated addresses, or an expression resolving to a list. |
cc | string | no | '' | — | only when {{ }} expressions only; otherwise stripped | As to. |
bcc | string | no | '' | — | only when {{ }} expressions only; otherwise stripped | As to. |
subject | string | no | '' | — | as written | Expressions allowed. |
body | string | no | '' | — | as written | Expressions allowed. |
bodyFormat | string | no | 'text' | text, markdown, html | as written | Markdown is sent as formatted HTML, with the Markdown as the plain-text part. |
attachments | list of strings | no | [] | — | as written | One expression per file, such as {{ upstream.<nodeId>.output.file }}. |
messageId | string | no | '' | — | as written | The Message-ID to send with. Empty: one is generated. See Email threading. |
inReplyTo | string | no | '' | — | as written | The Message-ID of the email this one answers. |
references | string | no | '' | — | as written | The thread's ids, separated by spaces or commas, or an expression resolving to a list. |
replyTo | string | no | '' | — | only when {{ }} expressions only; otherwise stripped | Where replies go. As to; counts towards the 50-recipient limit. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
messageId | string | Message-ID sent — store it to match replies (trigger.inReplyTo) |
accepted | string[] | Accepted recipients |
rejected | string[] | Rejected recipients |
Note: Store {{ upstream.<id>.output.messageId }} (e.g. in a Data Table row) to match a reply’s {{ trigger.inReplyTo }} or {{ trigger.references }} later.
Example (npx @elizon/cli example workflow --node send_email prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Send Email example
description: Thanks a customer by email. The SMTP server is set on the canvas after install.
graph:
nodes:
- id: request-form
type: trigger_form
position: { x: 0, y: 0 }
data:
authMode: none
fields:
- name: email
label: Your email
type: email
required: true
- name: machine
label: Machine
type: text
required: true
- id: thank-you
type: send_email
position: { x: 280, y: 0 }
data:
to: '{{ trigger.body.email }}'
subject: Thanks for your request
body: We received your request about the {{ trigger.body.machine }} and will reply today.
bodyFormat: text
attachments: []
edges:
- { id: request-form-to-thank-you, source: request-form, target: thank-you }http_request
The HTTP Request node (node page) calls an HTTP API.
- Expressions:
url, the query, header and body rows' names and values,queryJson,jsonBody,rawBody,binaryFileExpression, the multipart values. - Handles: one output;
{{ upstream.<node id>.output.body }},.status. - In a Package: the request travels.
authnever does: the credential is set on the canvas after install, and until then the request is sent without one. - Refusals:
authin the file is refused (PORTABLE_DEFINITION_INVALID).
Fields (from HttpRequestNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
method | string | no | 'GET' | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS | as written | The HTTP method. |
url | string | no | '' | — | as written | An absolute http:// or https:// URL. Expressions allowed. |
queryMode | string | no | 'rows' | rows, raw | as written | Query parameters as name/value rows, or one expression resolving to an object. |
queryRows | list of objects | no | [] | — | as written | The rows, for rows. |
queryRows[].id | string | no | '' | — | as written | — |
queryRows[].name | string | no | '' | — | as written | — |
queryRows[].value | string | no | '' | — | as written | — |
queryJson | string | no | '' | — | as written | The expression, for raw. |
headerRows | list of objects | no | [] | — | as written | Request headers. Both halves take expressions. |
headerRows[].id | string | no | '' | — | as written | — |
headerRows[].name | string | no | '' | — | as written | — |
headerRows[].value | string | no | '' | — | as written | — |
auth | object (one of the shapes below) | no | {"type":"none"} | — | never carried | none, connection (connectionId), basic (username, password), bearer (token), header (name, value) or query (name, value). Every credential is a {{secret.KEY}} reference. |
auth.type | string | yes | — | none, basic, bearer, header, query, connection | never carried | — |
auth.username | string | no | '' | — | never carried | With type: basic. |
auth.password | string | no | '' | — | never carried | With type: basic. |
auth.token | string | no | '' | — | never carried | With type: bearer. |
auth.name | string | no | '' | — | never carried | With type: header or type: query. |
auth.value | string | no | '' | — | never carried | With type: header or type: query. |
auth.connectionId | string | no | '' | — | never carried | With type: connection. |
bodyType | string | no | 'none' | none, json, form, multipart, raw, binary | as written | The kind of body. |
jsonMode | string | no | 'raw' | rows, raw | as written | A JSON body as name/value rows, or written out. |
jsonBody | string | no | '' | — | as written | The written-out JSON. Expressions are filled in before it is parsed. |
jsonRows | list of objects | no | [] | — | as written | The JSON body as rows. |
jsonRows[].id | string | no | '' | — | as written | — |
jsonRows[].name | string | no | '' | — | as written | — |
jsonRows[].value | string | no | '' | — | as written | — |
formRows | list of objects | no | [] | — | as written | A url-encoded form body. |
formRows[].id | string | no | '' | — | as written | — |
formRows[].name | string | no | '' | — | as written | — |
formRows[].value | string | no | '' | — | as written | — |
multipartFields | list of objects | no | [] | — | as written | A multipart body. kind is text or file; a file part's value points at a file. |
multipartFields[].id | string | no | '' | — | as written | — |
multipartFields[].name | string | no | '' | — | as written | — |
multipartFields[].kind | string | no | 'text' | text, file | as written | — |
multipartFields[].value | string | no | '' | — | as written | — |
rawBody | string | no | '' | — | as written | A raw body, sent verbatim. |
rawContentType | string | no | 'text/plain' | — | as written | Its content type. |
binaryFileExpression | string | no | '' | — | as written | The file to send as the whole body, for binary. |
responseFormat | string | no | 'auto' | auto, json, text, file | as written | How to read the response. |
responseFilename | string | no | 'response' | — | as written | The file name for a file response. |
fullResponse | boolean | no | true | — | as written | Output the full response, or only its body. |
neverError | boolean | no | false | — | as written | Turn a 4xx or 5xx into ordinary output instead of failing the node. |
timeoutSeconds | integer | no | 30 | 1–300 | as written | Request timeout. |
retries | integer | no | 0 | 0–5 | as written | Retries on network errors, 5xx and 429. |
followRedirects | boolean | no | true | — | as written | Follow 3xx responses. |
maxRedirects | integer | no | 5 | 0–5 | as written | Redirect hops to follow. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
status | number | HTTP status code |
statusText | string | HTTP status text |
headers | object | Response headers |
body | as the API returns | Response body |
file | file | Response saved as a file (file format only) |
request.method | string | Method sent |
request.url | string | URL called (credentials masked) |
request.headers | object | Headers sent (credentials masked) |
attempts | number | Attempts made (1 when no retry was needed) |
redirects | string[] | Redirect chain followed |
errorSuppressed | boolean | A 4xx/5xx was returned as normal output |
Note: With "Full response" off, this node's output IS the response body — the fields above are the envelope, so {{ upstream.<id>.output }} is the body itself.
Example (npx @elizon/cli example workflow --node http_request prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: HTTP Request example
description: Opens a helpdesk ticket. Any credential is set on the canvas after install.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: open-ticket
type: http_request
position: { x: 280, y: 0 }
data:
method: POST
url: https://helpdesk.example.com/api/tickets
bodyType: json
jsonMode: raw
jsonBody: '{"title": "{{ trigger.summary }}", "source": "web"}'
responseFormat: json
fullResponse: true
timeoutSeconds: 30
retries: 2
edges:
- { id: start-to-open-ticket, source: start, target: open-ticket }mcp_tool
The MCP Tool node (node page) calls one tool (toolName) of an MCP server.
- Expressions: each
argRows[].valuewithvalueMode: expression;argsJsonwithinputMode: json. - Handles: one output; the tool's answer is
{{ upstream.<node id>.output.structured }}(or.text). - In a Package: write
server: tools.<name>, an MCP Tool the Package declares (ornull);serverIdis never in the file.toolNameis the name the server lists, and is not checked offline:buildnever connects to the server.serverNameis a label. - Refusals:
servernaming an undeclared Tool (UNRESOLVED_LOCAL_REFERENCE).
Fields (from McpToolNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
server (stored serverId) | string | no | '' | — | as server: tools.<name>, or null | The MCP server, from Settings → Tools. |
serverName | string | no | '' | — | as written | Its name, kept for display. |
toolName | string | no | '' | — | as written | The tool to call, from the server's tool list. |
inputMode | string | no | 'manual' | manual, json | as written | Form (one field per argument) or JSON (one object). |
argRows | list of objects | no | [] | — | as written | The form's arguments. valueMode is fixed or expression; argType is the type the tool declared. |
argRows[].id | string | no | '' | — | as written | — |
argRows[].name | string | no | '' | — | as written | — |
argRows[].value | string | no | '' | — | as written | — |
argRows[].valueMode | string | no | 'fixed' | expression, fixed | as written | — |
argRows[].argType | string | no | 'unknown' | string, number, integer, boolean, object, array, unknown | as written | — |
argRows[].required | boolean | no | false | — | as written | — |
argsJson | string | no | '' | — | as written | The JSON arguments. Expressions are filled in before it is parsed. |
timeoutSeconds | integer | no | 30 | 1–300 | as written | Overrides the server's own timeout for this step. |
binaryHandling | string | no | 'file' | file, base64 | as written | Store images and audio as files, or keep the base64. |
continueOnToolError | boolean | no | false | — | as written | Turn a tool-reported error into output instead of failing the node. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
structured | object|null | Structured content (the tool’s own JSON) |
text | string | Text content, parts joined |
files | file[] | Image/audio parts stored as files |
content | array | Every content part as the server sent it |
toolName | string | Tool called |
serverId | string | MCP server id |
serverName | string | MCP server name |
isError | boolean | The tool reported an error and the run continued |
errorMessage | string|null | The tool's error message |
Note: A tool that reports an error FAILS this node unless "continue on tool error" is set — so on the ordinary path isError is false and errorMessage is null.
Example (npx @elizon/cli example workflow --node mcp_tool prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: MCP Tool example
description: Creates a contact through the Package’s MCP server.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: create-contact
type: mcp_tool
position: { x: 280, y: 0 }
data:
server: tools.crm
serverName: CRM
toolName: create_contact
inputMode: manual
argRows:
- id: arg-1
name: email
value: '{{ trigger.email }}'
valueMode: expression
argType: string
required: true
timeoutSeconds: 30
binaryHandling: file
continueOnToolError: false
edges:
- { id: start-to-create-contact, source: start, target: create-contact }ftp
The FTP node (node page) lists, downloads, uploads, renames or deletes files on an FTP, FTPS or SFTP server.
- Expressions:
pathanduploadFileExpression. - Handles: one output.
- In a Package: the operation travels. The server and login never do (
protocol,host,port,username,password,privateKey,passphrase,hostKeyFingerprint): they are set on the canvas after install. - Refusals: a connection field in the file is refused (
PORTABLE_DEFINITION_INVALID).
Fields (from FtpNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
protocol | string | no | 'sftp' | ftp, ftps, sftp | never carried | SFTP if the server offers it. |
host | string | no | '' | — | never carried | The server. |
port | integer | no | 0 | 0–65535 | never carried | 0 uses the protocol's default (22, 990, 21). For FTPS, 21 means explicit TLS. |
username | string | no | '' | — | never carried | The login. |
password | string | no | '' | — | never carried | Only a secret reference. |
privateKey | string | no | '' | — | never carried | SFTP key authentication. |
passphrase | string | no | '' | — | never carried | The private key's passphrase. |
hostKeyFingerprint | string | no | '' | — | never carried | SFTP: the server's expected SHA256:… host-key fingerprint. Required. |
operation | string | no | 'list' | list, download, upload, rename, delete | as written | rename is Move. |
path | string | no | '' | — | as written | The folder or file to act on. Expressions allowed. |
recursive | boolean | no | false | — | as written | List: include subfolders. |
uploadSource | string | no | 'file' | file, text | as written | Upload: a file from the run, or text you write. |
uploadFileExpression | string | no | '' | — | as written | Upload: the file, such as {{ upstream.<nodeId>.output.file }}. |
uploadText | string | no | '' | — | as written | Upload: the text. |
newPath | string | no | '' | — | as written | Move: the destination. |
createDirectories | boolean | no | false | — | as written | Upload / Move: create missing folders on the way. |
overwrite | boolean | no | false | — | as written | Move: replace a destination that is already there. Off, an existing destination stops the node. |
deleteFolder | boolean | no | false | — | as written | Delete: the path is a folder. |
deleteRecursive | boolean | no | false | — | as written | Delete: also delete everything inside it. |
connectTimeoutSeconds | integer | no | 20 | 1–120 | as written | Bounds reaching the server. |
transferTimeoutSeconds | integer | no | 120 | 1–900 | as written | Bounds one transfer or listing. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
protocol | string | Protocol ("ftp" | "ftps" | "sftp") |
host | string | Host connected to |
operation | string | Operation |
path | string | Remote path acted on |
affectedPaths | string[] | Every remote path this step touched |
More paths follow from the node’s own configuration; the node page lists them.
Note: Branch on {{ upstream.<id>.output.count }} (how many entries a List returned) or {{ upstream.<id>.output.deleted }} — both are plain values an If/Else can compare.
Example (npx @elizon/cli example workflow --node ftp prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: FTP example
description: Downloads the nightly export. The server and login are set on the canvas.
graph:
nodes:
- id: nightly
type: trigger_recurrence
position: { x: 0, y: 0 }
data:
mode: cron
cronExpression: 0 6 * * *
timezone: Europe/Berlin
- id: download
type: ftp
position: { x: 280, y: 0 }
data:
operation: download
path: /exports/orders.csv
connectTimeoutSeconds: 20
transferTimeoutSeconds: 120
edges:
- { id: nightly-to-download, source: nightly, target: download }app
The App node (node page) runs one operation of an app (Google Calendar, Contacts, Sheets, Drive, Gmail). Each app's page lists its resources, operations and inputs.
- Expressions: each
inputs.<name>.valuewithvalueMode: expression. - Handles: one output (see Output below).
- In a Package:
app,version,resource,operationandinputstravel. The account travels asconnection, acredentialSlot id, as for the App trigger. Add the app's provider tocompliance.providers. - Refusals: as for the App trigger, apart from the one-per-workflow rule.
Fields (from AppNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
app | string | no | '' | — | as written | The app: google-calendar, google-contacts, google-sheets, google-drive or google-gmail. |
version | string | no | '' | — | as written | The app definition's version the step was built against. |
resource | string | no | '' | — | as written | What it acts on — an event, a contact, a range, a file. Each app's page lists them. |
operation | string | no | '' | — | as written | What it does to it. Each app's page lists them, with their inputs. |
connection (stored connectionId) | string | no | '' | — | as connection: a credential Slot id, or null | The connected account, from Connections. |
inputs | map of name → object | no | {} | — | as written | One entry per input of the operation: { value, valueMode }, where valueMode is fixed or expression. |
inputs.<name>.value | string | no | '' | — | as written | — |
inputs.<name>.valueMode | string | no | 'fixed' | expression, fixed | as written | — |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
status | number | HTTP status the app answered with |
itemCount | number | Items returned (a list operation) |
pages | number | Pages fetched |
truncated | boolean | The page cap stopped the follow |
app | string | App |
resource | string | Resource |
operation | string | Operation |
More paths follow from the node’s own configuration; the node page lists them.
Note: truncated is true when the operation had more pages than its cap allows — result is then the first pages pages, not everything.
Example (npx @elizon/cli example workflow --node app prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: App example
description: Lists the next events of a Google Calendar.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: next-events
type: app
position: { x: 280, y: 0 }
data:
app: google-calendar
version: 1.0.0
resource: event
operation: list
connection: google-account
inputs:
calendarId:
value: primary
valueMode: fixed
timeMin:
value: '{{ run.startedAt }}'
valueMode: expression
edges:
- { id: start-to-next-events, source: start, target: next-events }Utilities
code
The Code node (node page) runs Python in a sandbox: main(input, files) gets inputExpression resolved, and the files of fileExpressions and tableInputs. Each entry of files has a name and a path to open; a table input's name is <tableInputs name>.jsonl (one JSON row per line).
- Expressions:
inputExpressionand each offileExpressions. - Handles: one output; a returned key is
{{ upstream.<node id>.output.result.<key> }}, and files written to/workspace/out/are{{ upstream.<node id>.output.files }}. - In a Package: everything travels. Each
tableInputsentry names its table astable: dataTables.<name>(tableIdis never in the file). The code is scanned byvalidateandbuild, and again by the installing Organization, whose result is the one the operator sees. - Refusals: a scan finding is a warning (
CODE_NODE_STATIC_SCAN_FLAGGED).tableInputs[].tablenaming an undeclared Data Table (UNRESOLVED_LOCAL_REFERENCE).
Fields (from CodeNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
language | string | no | 'python' | python | as written | Python 3.12. The field exists so other languages can be added later; Python is the only one today. |
code | string | no | '' | — | as written | Your code. It must define main(input, files). |
inputExpression | string | no | '' | — | as written | The value main receives as input. A single {{ }} keeps its type (a list stays a list); literal JSON is parsed. Empty is None. |
fileExpressions | list of strings | no | [] | at most 10 entries | as written | Up to 10. Each resolves to a file reference (an FTP download, an email attachment, a Convert to File output) or a list of them. |
tableInputs | list of objects | no | [] | at most 5 entries | converted per entry (see notes) | Up to 5 Data Tables read as files (see Reading a Data Table). Each entry is { name, tableId, where? }. |
tableInputs[].name | string | yes | — | — | converted per entry (see notes) | — |
tableInputs[].tableId | string | yes | — | — | converted per entry (see notes) | — |
tableInputs[].where | object | no | — | — | converted per entry (see notes) | — |
tableInputs[].where.match | string | no | 'all' | any, all | converted per entry (see notes) | — |
tableInputs[].where.conditions | list of objects | yes | — | at most 50 entries | converted per entry (see notes) | — |
tableInputs[].where.conditions[].column | string | yes | — | — | converted per entry (see notes) | — |
tableInputs[].where.conditions[].operator | string | yes | — | eq, neq, gt, gte, lt, lte, isEmpty, isNotEmpty, contains, startsWith | converted per entry (see notes) | — |
tableInputs[].where.conditions[].value | any | no | — | — | converted per entry (see notes) | — |
timeoutSeconds | integer | no | 30 | 1–120 | as written | The time limit, at most 120 seconds. Your deployment may set a lower maximum; the node's value is capped to it. |
Output
Read as {{ upstream.<node id>.output.<path> }} (an object):
| Path | Type | What it is |
|---|---|---|
result | json | What main() returned |
files | file[] | Files written to /workspace/out/ |
files.0 | file | First output file |
stdout | string | Printed output (truncated) |
durationMs | number | Run time (ms) |
memoryBytes | number | Peak memory (bytes) |
Note: Read a returned key as {{ upstream.<id>.output.result.<key> }}. For large data, write a file to /workspace/out/ and pass {{ upstream.<id>.output.files.0 }} on.
Example (npx @elizon/cli example workflow --node code prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Code example
description: Counts the rows of the Package’s table with a few lines of Python.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: An operator starts this run from the web app.
- id: count
type: code
position: { x: 280, y: 0 }
data:
language: python
code: |
import json
def main(input, files):
table = next(f for f in files if f["name"] == "orders.jsonl")
with open(table["path"]) as fh:
rows = [json.loads(line) for line in fh if line.strip()]
return {"rows": len(rows), "asked_by": input}
inputExpression: '{{ trigger.request }}'
fileExpressions: []
tableInputs:
- name: orders
table: dataTables.orders
timeoutSeconds: 30
edges:
- { id: start-to-count, source: start, target: count }What main() returns is under output.result
main() returning {"rows": 3} is read as {{ upstream.<id>.output.result.rows }};
output.rows does not exist. The Code node's output is { result, files, stdout, durationMs, memoryBytes }, so every returned key goes through .result. See
Reading another node's output
for every node type's fields.
note
The Note node (node page) is text on the canvas. It never runs and has no handles, so no edge reaches or leaves it.
- Expressions: none.
- Handles: none.
- In a Package:
texttravels. Do not name people in it: the Package travels to people who should not receive their addresses. - Refusals: an identifier-shaped (UUID) value in the text (
PORTABLE_DEFINITION_INVALID).
Fields (from NoteNodeDataSchema; each row is a key of the node's data)
| Field | Type | Required | Default | Allowed values | In the file | Description |
|---|---|---|---|---|---|---|
text | string | no | '' | — | as written | The note's text. |
Output
Nothing a later node reads.
Example (npx @elizon/cli example workflow --node note prints it with its manifest)
# A portable Workflow definition: the nodes, and the edges between them.
name: Note example
description: A note on the canvas. It has no handles, so no edge reaches it.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data:
description: Run it from Studio.
- id: why
type: note
position: { x: 280, y: 0 }
data:
text: Operations runs this on the first of the month.
edges: []