Elizon Docs
Features & SpecsPortable definitions

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 validate and build refuse.
  • Fields, generated from the node's data schema. "In the file" says what the Package does with each field: as written travels unchanged, as agent (and the like) means you write the reference instead of the stored id, and never carried means the field is left out of the file. Rows such as branches[].id are the fields of each entry of a list; inputs.<name>.value is 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 passes validate and build.

For several nodes working together, run npx @elizon/cli example workflow --pattern <name>:

PatternWhat it shows
if-elseAn If/Else node with two branches and a default, each wired by its sourceHandle.
loop-send-emailA Data Table read, a Loop over the rows with a Send Email in its body, and a step after the loop.
human-review-upsertA single and a batch Human Review, each followed by a Data Table upsert with its key condition.
code-bulk-upsertA 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:

NodesourceHandle values
if_elseThe id of each entry of branches, and defaultBranchId (default unless you change it). Exactly one is taken.
classifycategory-<id> for each entry of categories (category-billing for id: billing), and category-other when noMatchAction is other_branch.
looploop 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.

edges
- { 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.

CodeWhen
UNRESOLVED_LOCAL_REFERENCEvalidate and build: a reference (agents., teams., tools., knowledgeBases., dataTables.) names nothing the manifest declares.
PORTABLE_UNSUPPORTED_NODE_TYPEbuild: the type is not one of the 27, for example a retired type from an old saved version.
PORTABLE_DEFINITION_INVALIDbuild: 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: description travels. 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
descriptionstringno''—as writtenA 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)

workflows/trigger-manual-example.yaml
# 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
modestringno'interval'interval, cronas writtenInterval for "every N units", Cron for a specific wall-clock time.
intervalValueintegerno1≥ 1as writtenThe N in "every N units". Interval mode only.
intervalUnitstringno'days'minutes, hours, days, weeksas writtenThe unit. Interval mode only.
cronExpressionstringno''—as writtenA five-field cron expression, for example 0 9 * * 1-5 (09:00 on weekdays). Cron mode only.
timezonestringno'UTC'—as writtenThe 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)

workflows/trigger-recurrence-example.yaml
# 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: method and fields travel. webhookSecret is 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
methodstringno'POST'GET, POST, PUT, PATCH, DELETEas writtenThe HTTP method the trigger documents.
webhookSecretstringno''—never carriedThe signing secret for HMAC-signed calls. Studio shows it next to the URL; treat it as a credential.
fieldslist of objectsno[]—as writtenThe request-body fields. type is string, number, boolean or object. Studio shows the matching OpenAPI request body beside them.
fields[].namestringyes——as written—
fields[].typestringyes—string, number, boolean, objectas written—
fields[].requiredbooleanyes——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)

workflows/trigger-http-example.yaml
# 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: fields and authMode travel. basicAuthUsername is never carried, and the password is never on the node: with authMode: 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
fieldslist of objectsno[]—as writtenThe form's questions, in order.
fields[].namestringyes——as written—
fields[].typestringyes—text, textarea, number, email, password, date, dropdown, radio, checkboxes, file, hiddenas written—
fields[].requiredbooleanyes——as written—
fields[].labelstringyes——as written—
fields[].optionslist of stringsno——as written—
fields[].acceptedExtensionslist of stringsno——as written—
fields[].allowMultiplebooleanno——as written—
authModestringno'none'none, basic, elizon_loginas writtenWho can open and submit the form — see The three auth modes.
basicAuthUsernamestringno——never carriedThe 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)

workflows/trigger-form-example.yaml
# 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, mailbox and moveToMailbox are 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
hoststringno''—never carriedThe IMAP server.
portintegerno9931–65535never carried993 is IMAPS; STARTTLS and plain connections normally use 143.
securitystringno'tls'tls, starttls, nonenever carriedSee Connecting a mailbox.
usernamestringno''—never carriedThe account's login.
passwordstringno''—never carriedOnly a secret reference. A typed password is refused by the save.
mailboxstringno'INBOX'—never carriedThe folder to watch, exactly as the server names it.
postActionstringno'markRead'markRead, move, noneas writtenWhat to do to the message after its run starts — see After processing.
moveToMailboxstringno''—never carriedThe folder for move. It must already exist.
filterUnseenbooleannotrue—as writtenOnly unread mail.
filterFromstringno''—as writtenSender address contains.
filterTostringno''—as writtenRecipient address contains.
filterSubjectContainsstringno''—as writtenSubject contains.
filterSincestringno''—as writtenArrived on or after this day.
downloadAttachmentsbooleannofalse—as writtenBring attachments into the run as files.
attachmentExtensionsstringno''—as writtenComma-separated extensions to keep (pdf,xlsx). Empty keeps every type.
attachmentMaxBytesintegerno104857601–26214400as writtenLarger attachments are skipped; the email still runs.
formatstringno'simple'simple, rawas writtenraw also adds {{ trigger.raw }}, the whole message source.
pollMinutesintegerno51–60as writtenHow often to check.
processExistingUnreadbooleannofalse—as writtenOn the first check, also start runs for mail already in the folder.
processExistingMaxintegerno101–200as writtenHow 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)

workflows/trigger-email-example.yaml
# 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. knowledgeBaseName is a label and is replaced by that Knowledge Base's name. Declare knowledge in compatibility.requiredCapabilities.
  • Refusals: knowledgeBase missing or null (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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
knowledgeBase (stored knowledgeBaseId)stringno''—as knowledgeBase: knowledgeBases.<name>The knowledge base to watch, from this workflow's project. See Choosing the knowledge base.
knowledgeBaseNamestringno''—as writtenIts name, kept for display so the node still says what it watched if the knowledge base is later deleted.
eventstringno'indexed'indexedas writtenThe 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)

workflows/trigger-knowledge-document-example.yaml
# 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: inputs entries take a valueMode. Later nodes read the item as {{ trigger.item.<field> }}.
  • Handles: one output.
  • In a Package: app, trigger, inputs and the poll settings travel. A fixed value in inputs (the example's folderId) 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 as connection: the id of a credential Slot with the app's account (app: google), which the operator binds to one of their connected accounts at install. Add the app's provider to compliance.providers.
  • Refusals: connection naming an undeclared Slot, a Slot that is not type: credential, or one whose app is 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
appstringno''—as writtenThe app, for example google-drive.
triggerstringno''—as writtenWhich of that app's triggers, for example file-added. Each app's page lists them.
connection (stored connectionId)stringno''—as connection: a credential Slot id, or nullThe connected account to watch, from Connections.
inputsmap of name → objectno{}—as writtenThe trigger's own settings — the folder, the calendar, the sheet range. Each app's page lists them.
inputs.<name>.valuestringno''—as written—
inputs.<name>.valueModestringno'fixed'expression, fixedas written—
pollMinutesintegerno51–60as writtenHow often to check.
processExistingbooleannofalse—as writtenOn the first check, also start runs for items that already exist.
processExistingMaxintegerno101–200as writtenHow 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)

workflows/trigger-app-example.yaml
# 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: existing and agent: agents.<name> (or null for none chosen yet, which validate and build warn about with WORKFLOW_NODE_UNBOUND: the node installs but cannot run); the stored agentId is never in the file. agentName is a label and is replaced by the installed Agent's name. The inline mode (instructions, model and tools on the node itself) is still decoded, with llm_slot for its model, tools[].tool: tools.<name> (MCP servers only) and knowledgeSources[].knowledgeBase: knowledgeBases.<name>, but Studio no longer saves new inline nodes: use an Agent.
  • Refusals: agent naming an undeclared Agent (UNRESOLVED_LOCAL_REFERENCE).

Fields (from AiAgentNodeDataSchema; each row is a key of the node's data)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
modestringyes—existing, inlineas writtenAlways 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)stringyes——as agent: agents.<name>, or nullWith mode: existing. The agent to run. It must belong to the workflow's organization.
agentNamestringno——as writtenWith mode: existing. The agent's name, kept for display.
inputMappingslist of objectsyes——as writtenWith 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[].fieldstringyes——as writtenWith mode: existing.
inputMappings[].expressionstringyes——as writtenWith mode: existing.
instructionsstringyes——as writtenWith mode: inline.
llm_slot (stored llm_config_id)string or nullno——as llm_slot: an llm Slot idWith mode: inline.
model_idstring or nullno——as writtenWith mode: inline.
toolslist of objectsyes——converted per entry (see notes)With mode: inline.
tools[].typestringyes—mcp_server, connectorconverted per entry (see notes)With mode: inline.
tools[].idstringyes——converted per entry (see notes)With mode: inline.
tools[].namestringyes——converted per entry (see notes)With mode: inline.
knowledgeSourceslist of objectsyes——converted per entry (see notes)With mode: inline.
knowledgeSources[].typestringyes—knowledge_base, urlconverted per entry (see notes)With mode: inline.
knowledgeSources[].idstringno——converted per entry (see notes)With mode: inline.
knowledgeSources[].urlstringno——converted per entry (see notes)With mode: inline.
knowledgeSources[].labelstringyes——converted per entry (see notes)With mode: inline.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
textstringResponse text
jsonobject|nullStructured JSON (if the agent emitted any)
modelstringModel used
toolCallsarrayTool calls
knowledgeQueriesCountnumberKnowledge queries count
tokenUsage.promptnumberPrompt tokens
tokenUsage.completionnumberCompletion tokens
estimatedCostUsdnumberEstimated cost (USD)
latencyMsnumberLatency (ms)

Example (npx @elizon/cli example workflow --node ai_agent prints it with its manifest)

workflows/ai-agent-example.yaml
# 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> (or null, which validate and build warn about with WORKFLOW_NODE_UNBOUND: the node installs but cannot run); teamId is never in the file. teamName is replaced by the installed Team's name. Declare teams in compatibility.requiredCapabilities.
  • Refusals: team naming an undeclared Team (UNRESOLVED_LOCAL_REFERENCE).

Fields (from TeamStepNodeDataSchema; each row is a key of the node's data)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
team (stored teamId)stringyes——as team: teams.<name>, or nullThe team to run. It must belong to the workflow's organization.
teamNamestringno——as writtenThe team's name, kept for display.
inputExpressionstringno''—as writtenWhat 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):

PathTypeWhat it is
textstringResponse text
teamIdstringTeam id
runIdstring|nullRun id
sessionIdstring|nullSession id
tokenUsage.promptnumberPrompt tokens
tokenUsage.completionnumberCompletion tokens
latencyMsnumberLatency (ms)

Example (npx @elizon/cli example workflow --node team_step prints it with its manifest)

workflows/team-step-example.yaml
# 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>, an llm Slot the manifest declares; the operator binds it to one of their LLM Configurations at install. model_id is a preference, used when that Configuration offers the model. Everything else travels.
  • Refusals: a value Studio could not save (PORTABLE_DEFINITION_INVALID). An llm_slot that names no declared Slot is not caught by validate or build today; install refuses it (PORTABLE_UNRESOLVED_BINDING). Check the Slot id yourself.

Fields (from AiPromptNodeDataSchema; each row is a key of the node's data)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
llm_slot (stored llm_config_id)string or nullno——as llm_slot: an llm Slot idThe LLM provider connection to use, from Settings → LLM Configurations.
model_idstring or nullno——as writtenThe model on that connection.
instructionsstringno''—as writtenThe system prompt. May contain {{ }} references. Studio keeps a version history of it; the active version is what runs.
inputExpressionstringno''—as writtenThe user message, with {{ }} references. Empty forwards what reached the node, the same way the AI Agent node does.
attachmentExpressionstringno''—as writtenOne 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.
temperaturenumberno0.70–2as writtenSampling temperature. Lower is more repeatable.
maxTokensnumberno40961–128000as writtenThe answer's token limit.
outputFormatstringno'text'text, structured, customas writtenText passes the answer through. Structured asks for, and checks, one of three fixed JSON shapes. Custom asks for, and checks, your own JSON Schema.
structuredSchemaIdstringno——as writtenThe fixed shape for Structured: classification is { category, confidence, summary }, sentiment is { sentiment, confidence }, extraction is { fields: { … } }.
customJsonSchemastringno——as writtenA 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):

PathTypeWhat it is
completionTextstringCompletion text
structuredOutputunknownStructured output (only when Output format is Structured/Custom)
modelUsedstringModel used
tokenUsage.promptnumberPrompt tokens
tokenUsage.completionnumberCompletion tokens
latencyMsnumberLatency (ms)

Example (npx @elizon/cli example workflow --node ai_prompt prints it with its manifest)

workflows/ai-prompt-example.yaml
# 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, plus category-other with noMatchAction: other_branch. With allowMultipleClasses: true several handles can fire; with noMatchAction: discard an unmatched input fires none.
  • In a Package: llm_slot as for AI Prompt. Category ids are local to the graph: Studio writes UUIDs, and a short id such as billing works 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
llm_slot (stored llm_config_id)string or nullno——as llm_slot: an llm Slot idThe LLM provider connection to use.
model_idstring or nullno——as writtenThe model on that connection.
inputPromptstringno'{{ trigger.body.text }}'—as writtenThe text to classify, with {{ }} references. Empty forwards what reached the node.
categorieslist of objectsyes——as writtenAt 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[].idstringyes——as written—
categories[].namestringyes——as written—
categories[].descriptionstringyes——as written—
allowMultipleClassesbooleannofalse—as writtenLet the model pick several categories. Every matching branch then runs.
noMatchActionstringno'discard'discard, other_branchas writtenWhen nothing matches: discard stops this branch of the run; other_branch continues down the node's Other handle.
systemPromptTemplatestringno——as writtenReplace the platform's classification prompt. Empty uses the default.
enableAutoFixingbooleannofalse—as writtenWhen 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):

PathTypeWhat it is
inputTextstringInput text
assignedCategoriesstring[]Assigned categories
discardedbooleanDiscarded (no match)
modelUsedstringModel used
tokenUsage.promptnumberPrompt tokens
tokenUsage.completionnumberCompletion tokens
latencyMsnumberLatency (ms)
autoFixAppliedbooleanAuto-fix applied

Example (npx @elizon/cli example workflow --node classify prints it with its manifest)

workflows/classify-example.yaml
# 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, and defaultBranchId. 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 (and branch-1 for the first), and a short id such as large works the same.
  • Refusals: only the ones every node has.

Fields (from IfElseNodeDataSchema; each row is a key of the node's data)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
brancheslist of objectsno[]at most 5 entriesas writtenThe conditions, tested top to bottom. id is the branch's output handle; label is what the canvas shows.
branches[].idstringyes——as written—
branches[].labelstringyes——as written—
branches[].expressionstringyes——as written—
defaultBranchIdstringno'default'—as writtenThe handle taken when no condition holds — the Else branch.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
branchTakenstringBranch id taken

Example (npx @elizon/cli example workflow --node if_else prints it with its manifest)

workflows/if-else-example.yaml
# 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) and done (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 (output itself 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
sourceArraystringno''—as writtenA {{ }} expression that resolves to a list, for example {{ trigger.body.orders }} or {{ upstream.search.output.rows }}.
batchSizeintegerno1≥ 1as writtenHow 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)

workflows/loop-example.yaml
# 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
modestringno'interval'interval, untilas writtenAfter time interval or At specified time.
amountintegerno1≥ 1as writtenHow long, in unit. Interval mode.
unitstringno'hours'seconds, minutes, hours, daysas writtenInterval mode.
atstringno''—as writtenThe date and time to resume at, or a {{ }} expression that resolves to one. Until mode.
timezonestringno'UTC'—as writtenThe zone at is read in. Ignored when at carries its own offset or Z.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
modestringMode ("interval" | "until")
resumeAtstringWhen the run resumed (ISO 8601, UTC)
timezonestringThe node's time zone
waitedMsnumberHow long the run waited (ms)
alreadyPassedbooleanTarget time had already passed
messagestringWhat the node did, in words

Example (npx @elizon/cli example workflow --node wait prints it with its manifest)

workflows/wait-example.yaml
# 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 field default with valueMode: expression. assignedTo too: see below.
  • Handles: one output. Branch on {{ upstream.<node id>.output.decision }} (approved or rejected) with an If/Else node; a batch review's approved list feeds a Loop.
  • In a Package: the file carries approverRequired. Leave assignedTo out and set approverRequired: true, and the approvers are collected at install (in the wizard, or --approver.<node id>=…). Or set assignedTo to {{ }} expressions only (for example '{{ trigger.body.managerEmail }}') and approverRequired: false: it travels, and the installer's answer becomes the fallback. fallbackAssignedTo is never carried.
  • Refusals: a literal address in assignedTo is stripped by build, with the warning HUMAN_REVIEW_APPROVER_STRIPPED (from validate too).

Fields (from HumanReviewNodeDataSchema; each row is a key of the node's data)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
titlestringno''—as writtenWhat the reviewer sees as the request's title. May contain {{ }} references.
messagestringno''—as writtenThe request's body. May contain {{ }} references.
assignedTostringno''—only when {{ }} expressions only; otherwise strippedWho reviews: email addresses, {{ }} expressions, or a mix, comma-separated — for example {{ upstream.lookup.output.result.accountOwnerEmail }}. Resolved when the step runs (see below).
fallbackAssignedTostringno''—never carriedLiteral email addresses used when none of the resolved approvers can review.
responseFieldslist of objectsno[]—as writtenWhat 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[].namestringyes——as written—
responseFields[].typestringyes—text, long_text, html, yes_no, email, number, dateas written—
responseFields[].defaultobjectno——as written—
responseFields[].default.valuestringyes——as written—
responseFields[].default.valueModestringyes—fixed, expressionas written—
timeoutValueintegerno24≥ 1as writtenHow long to wait, in timeoutUnit. Between 1 hour and 30 days in total.
timeoutUnitstringno'hours'hours, daysas writtenThe unit of timeoutValue.
onTimeoutstringno'expire'expire, approve, rejectas writtenWhat happens when nobody answers in time (see Timeouts).
modestringno'single'single, batchas writtenbatch reviews a list of items in one request — see Batch review.
itemsExpressionstringno''—as writtenBatch only. An expression that resolves to a list of objects, e.g. {{ upstream.drafts.output.rows }}.
itemLabelstringno''—as writtenBatch only. Each item's title, an expression over item, e.g. {{ item.customerName }} — {{ item.invoiceNumber }}. Blank shows "Item 1", "Item 2", …
itemFieldslist of objectsno[]—as writtenBatch only. The editable fields per item, with the same types as responseFields. A default may read the item: {{ item.draftBody }}.
itemFields[].namestringyes——as written—
itemFields[].typestringyes—text, long_text, html, yes_no, email, number, dateas written—
itemFields[].defaultobjectno——as written—
itemFields[].default.valuestringyes——as written—
itemFields[].default.valueModestringyes—fixed, expressionas written—

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
outcomestringOutcome ("responded" | "timed_out")
decisionstringDecision ("approved" | "rejected")
resolvedBystringResolved by ("reviewer" | "timeout")
requestIdstringRequest id
timeoutAtstringTimeout deadline
responderEmailstring|nullResponder email (responded only)
respondedAtstring|nullResponded at (responded only)
droppedApproversstring[]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)

workflows/human-review-example.yaml
# 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
namestringno''—as writtenThe variable's name, read as {{ variables.<name> }}.
typestringno'string'string, number, boolean, objectas writtenWhat the value is converted to.
initialValuestringno''—as writtenThe 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):

PathTypeWhat it is
valuedeclaredValue (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)

workflows/variable-example.yaml
# 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[].value with valueMode: 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
rowslist of objectsno[]—as writtenOne field each. valueMode is expression (the default: value is resolved) or fixed (value is used as written).
rows[].idstringyes——as written—
rows[].namestringyes——as written—
rows[].valuestringyes——as written—
rows[].valueModestringno'expression'expression, fixedas written—
keepOnlySetFieldsbooleannotrue—as writtenThe 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)

workflows/edit-fields-example.yaml
# 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
sourceExpressionstringno''—as writtenWhat to write, usually an expression such as {{ upstream.report-writer.output.text }}.
formatstringno'md'md, txt, json, csv, html, pdf, docx, xlsxas writtenThe file format.
filenamestringno'report'—as writtenExpressions allowed. The format's extension is added when the name lacks it.
shareWithstringno'workflow'workflow, projectas writtenThis workflow only, or Every workflow in this project — see below.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
filefileFile (attach it, or read it with AI Prompt)
file.filenamestringFile name
file.contentTypestringContent type
file.sizeBytesnumberSize (bytes)
file.textstringText content

Example (npx @elizon/cli example workflow --node convert_to_file prints it with its manifest)

workflows/convert-to-file-example.yaml
# 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[].value and automaticSource.value with valueMode: 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 (or null); tableId is never in the file. tableName is a label and is replaced by the installed table's name, except on a table create, where it is the name created. An update, upsert or delete needs a key: a conditions entry naming a column (bulkUpsert keys on keyColumns instead).
  • Refusals: table naming 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
resourcestringno'row'table, rowas writtenWhat the operation acts on. See Operations at a glance.
operationstringno'insert'create, list, rename, delete, addColumn, removeColumn, insert, get, update, upsert, bulkUpsert, existsas writtenThe operation, for example insert, get, update, upsert, bulkUpsert, delete, exists, create, list, rename, addColumn, removeColumn.
table (stored tableId)stringno''—as table: dataTables.<name>, or nullThe table, from this workflow's project. See Choosing the table.
tableNamestringno''—as writtenThe table's name — kept for display, and the name for Create table.
columnslist of objectsno[]—as writtenCreate table: the columns, each a name and a type.
columns[].idstringno——as written—
columns[].namestringno''—as written—
columns[].typestringno'string'boolean, date, number, stringas written—
returnExistingIfExistsbooleannofalse—as writtenCreate table: return the table if one by that name exists, instead of failing.
newTableNamestringno''—as writtenRename table: the new name.
columnNamestringno''—as writtenAdd / Remove column: the column.
columnTypestringno'string'boolean, date, number, stringas writtenAdd column: its type.
mappingstringno'manual'manual, automaticas writtenHow a write's cells are set. See Writing cells.
mappingRowslist of objectsno[]—as writtenManual mapping: one row per column.
mappingRows[].idstringno''—as written—
mappingRows[].valuestringno''—as written—
mappingRows[].valueModestringno'expression'expression, fixedas written—
mappingRows[].columnstringno''—as written—
automaticSourceobjectno{"id":"","value":"","valueMode":"expression"}—as writtenAutomatic mapping: where the values come from.
automaticSource.idstringno''—as written—
automaticSource.valuestringno''—as written—
automaticSource.valueModestringno'expression'expression, fixedas written—
conditionslist of objectsno[]—as writtenWhich rows to match. See Conditions.
conditions[].idstringno''—as written—
conditions[].valuestringno''—as written—
conditions[].valueModestringno'expression'expression, fixedas written—
conditions[].columnstringno''—as written—
conditions[].operatorstringno'eq'eq, neq, gt, gte, lt, lte, isEmpty, isNotEmpty, contains, startsWithas written—
conditionMatchstringno'all'any, allas writtenWhether every condition or any one must hold.
orderBylist of objectsno[]—as writtenSort order for Get and List tables.
orderBy[].idstringno''—as written—
orderBy[].fieldstringno''—as written—
orderBy[].directionstringno'asc'asc, descas written—
limitinteger or nullnonull≥ 1as writtenHow many rows or tables to return.
returnAllbooleannofalse—as writtenGet: return every matching row.
keyColumnslist of stringsno[]—as writtenBulk upsert: the column names a row is matched on. See Bulk upsert by key.
rowsSourceModestringno'array'array, fileas writtenBulk upsert and an automatic insert: whether the rows come from an array expression or a file.
rowsFileExpressionstringno''—as writtenThe file holding the rows (JSON array or JSONL), when rowsSourceMode is file.
cursorstringno''—as writtenGet: the nextCursor of a previous page. See Paging with a cursor.
readOutputstringno'rows'rows, fileas writtenGet: a page of rows, or every matching row as one JSONL file.
dryRunbooleannofalse—as writtenReport what a destructive operation would do, without doing it.
optimizeBulkbooleannofalse—as writtenInsert: write many rows in one statement.
applyToAllRowsbooleannofalse—as writtenUpdate / Delete with no conditions: confirm it is meant for every row.
allowTableDeletebooleannofalse—as writtenDelete table: confirm it.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
resourcestringResource ("table" | "row")
operationstringOperation
table.idstringTable id
table.namestringTable name
affectednumberRows (or tables) the operation affected
dryRunbooleanRan 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)

workflows/data-table-example.yaml
# 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 }

The Knowledge Search node (node page) searches a Knowledge Base by query, metadataFilters and documentName, and returns up to maxResults documents.

  • Expressions: query, and metadataFilters[].value with valueMode: expression.
  • Handles: one output; {{ upstream.<node id>.output.documents }} and .count.
  • In a Package: write knowledgeBase: knowledgeBases.<name>, a Knowledge Base the Package declares; knowledgeBaseName is replaced by the installed one's name. A filter's field is one of that Knowledge Base's indexConfig.metadata.fields.
  • Refusals: knowledgeBase missing or null (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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
knowledgeBase (stored knowledgeBaseId)stringno''—as knowledgeBase: knowledgeBases.<name>The knowledge base to search, from this workflow's project.
knowledgeBaseNamestringno''—as writtenIts name, kept for display on the node card.
querystringno''—as writtenOptional. Text or an expression. Empty means an exact lookup.
metadataFilterslist of objectsno[]at most 20 entriesas writtenEach row compares one of the knowledge base's declared metadata fields with a value, Fixed or an expression. All rows must match (AND).
metadataFilters[].idstringno''—as written—
metadataFilters[].fieldstringno''—as written—
metadataFilters[].valuestringno''—as written—
metadataFilters[].valueModestringno'fixed'expression, fixedas written—
documentNameobjectno——as writtenOptional. Matches the document's filename, case-insensitive.
documentName.operatorstringno'contains'equals, containsas written—
documentName.valuestringno''—as written—
maxResultsintegerno101–50as writtenHow many documents to return, 1–50.
includeFilesbooleannofalse—as writtenAdds each document's original as a file you can attach. See Files.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
documentsobject[]Documents found
countnumberHow many documents
documents.0.documentNamestringFirst document’s name
documents.0.documentIdstringFirst document’s id
documents.0.sourceobjectFirst document’s source (citation)
documents.0.metadatajsonFirst document’s metadata
documents.0.scorenumber|nullFirst document’s score (query only)
documents.0.excerptstring|nullFirst document’s best excerpt (query only)
documents.0.filefile|nullFirst 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)

workflows/knowledge-search-example.yaml
# 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 of attachments, and the threading ids messageId, 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, bcc and replyTo travel only when they are {{ }} expressions only, such as '{{ loop.item.Email }}'.
  • Refusals: a literal recipient is stripped by build, with the warning SEND_EMAIL_RECIPIENT_STRIPPED (from validate too). An SMTP field in the file is refused (PORTABLE_DEFINITION_INVALID).

Fields (from SendEmailNodeDataSchema; each row is a key of the node's data)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
smtpHoststringno''—never carriedThe SMTP server.
smtpPortintegerno5871–65535never carriedUsually 587 for STARTTLS and 465 for TLS.
smtpSecuritystringno'starttls'tls, starttls, nonenever carriednone is for a relay on a trusted network only.
smtpUsernamestringno''—never carriedEmpty for a server that needs no login.
smtpPasswordstringno''—never carriedOnly a secret reference.
fromAddressstringno''—never carriedThe sender address.
fromNamestringno''—never carriedThe sender's display name.
tostringno''—only when {{ }} expressions only; otherwise strippedComma-separated addresses, or an expression resolving to a list.
ccstringno''—only when {{ }} expressions only; otherwise strippedAs to.
bccstringno''—only when {{ }} expressions only; otherwise strippedAs to.
subjectstringno''—as writtenExpressions allowed.
bodystringno''—as writtenExpressions allowed.
bodyFormatstringno'text'text, markdown, htmlas writtenMarkdown is sent as formatted HTML, with the Markdown as the plain-text part.
attachmentslist of stringsno[]—as writtenOne expression per file, such as {{ upstream.<nodeId>.output.file }}.
messageIdstringno''—as writtenThe Message-ID to send with. Empty: one is generated. See Email threading.
inReplyTostringno''—as writtenThe Message-ID of the email this one answers.
referencesstringno''—as writtenThe thread's ids, separated by spaces or commas, or an expression resolving to a list.
replyTostringno''—only when {{ }} expressions only; otherwise strippedWhere replies go. As to; counts towards the 50-recipient limit.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
messageIdstringMessage-ID sent — store it to match replies (trigger.inReplyTo)
acceptedstring[]Accepted recipients
rejectedstring[]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)

workflows/send-email-example.yaml
# 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. auth never does: the credential is set on the canvas after install, and until then the request is sent without one.
  • Refusals: auth in the file is refused (PORTABLE_DEFINITION_INVALID).

Fields (from HttpRequestNodeDataSchema; each row is a key of the node's data)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
methodstringno'GET'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONSas writtenThe HTTP method.
urlstringno''—as writtenAn absolute http:// or https:// URL. Expressions allowed.
queryModestringno'rows'rows, rawas writtenQuery parameters as name/value rows, or one expression resolving to an object.
queryRowslist of objectsno[]—as writtenThe rows, for rows.
queryRows[].idstringno''—as written—
queryRows[].namestringno''—as written—
queryRows[].valuestringno''—as written—
queryJsonstringno''—as writtenThe expression, for raw.
headerRowslist of objectsno[]—as writtenRequest headers. Both halves take expressions.
headerRows[].idstringno''—as written—
headerRows[].namestringno''—as written—
headerRows[].valuestringno''—as written—
authobject (one of the shapes below)no{"type":"none"}—never carriednone, connection (connectionId), basic (username, password), bearer (token), header (name, value) or query (name, value). Every credential is a {{secret.KEY}} reference.
auth.typestringyes—none, basic, bearer, header, query, connectionnever carried—
auth.usernamestringno''—never carriedWith type: basic.
auth.passwordstringno''—never carriedWith type: basic.
auth.tokenstringno''—never carriedWith type: bearer.
auth.namestringno''—never carriedWith type: header or type: query.
auth.valuestringno''—never carriedWith type: header or type: query.
auth.connectionIdstringno''—never carriedWith type: connection.
bodyTypestringno'none'none, json, form, multipart, raw, binaryas writtenThe kind of body.
jsonModestringno'raw'rows, rawas writtenA JSON body as name/value rows, or written out.
jsonBodystringno''—as writtenThe written-out JSON. Expressions are filled in before it is parsed.
jsonRowslist of objectsno[]—as writtenThe JSON body as rows.
jsonRows[].idstringno''—as written—
jsonRows[].namestringno''—as written—
jsonRows[].valuestringno''—as written—
formRowslist of objectsno[]—as writtenA url-encoded form body.
formRows[].idstringno''—as written—
formRows[].namestringno''—as written—
formRows[].valuestringno''—as written—
multipartFieldslist of objectsno[]—as writtenA multipart body. kind is text or file; a file part's value points at a file.
multipartFields[].idstringno''—as written—
multipartFields[].namestringno''—as written—
multipartFields[].kindstringno'text'text, fileas written—
multipartFields[].valuestringno''—as written—
rawBodystringno''—as writtenA raw body, sent verbatim.
rawContentTypestringno'text/plain'—as writtenIts content type.
binaryFileExpressionstringno''—as writtenThe file to send as the whole body, for binary.
responseFormatstringno'auto'auto, json, text, fileas writtenHow to read the response.
responseFilenamestringno'response'—as writtenThe file name for a file response.
fullResponsebooleannotrue—as writtenOutput the full response, or only its body.
neverErrorbooleannofalse—as writtenTurn a 4xx or 5xx into ordinary output instead of failing the node.
timeoutSecondsintegerno301–300as writtenRequest timeout.
retriesintegerno00–5as writtenRetries on network errors, 5xx and 429.
followRedirectsbooleannotrue—as writtenFollow 3xx responses.
maxRedirectsintegerno50–5as writtenRedirect hops to follow.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
statusnumberHTTP status code
statusTextstringHTTP status text
headersobjectResponse headers
bodyas the API returnsResponse body
filefileResponse saved as a file (file format only)
request.methodstringMethod sent
request.urlstringURL called (credentials masked)
request.headersobjectHeaders sent (credentials masked)
attemptsnumberAttempts made (1 when no retry was needed)
redirectsstring[]Redirect chain followed
errorSuppressedbooleanA 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)

workflows/http-request-example.yaml
# 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[].value with valueMode: expression; argsJson with inputMode: 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 (or null); serverId is never in the file. toolName is the name the server lists, and is not checked offline: build never connects to the server. serverName is a label.
  • Refusals: server naming an undeclared Tool (UNRESOLVED_LOCAL_REFERENCE).

Fields (from McpToolNodeDataSchema; each row is a key of the node's data)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
server (stored serverId)stringno''—as server: tools.<name>, or nullThe MCP server, from Settings → Tools.
serverNamestringno''—as writtenIts name, kept for display.
toolNamestringno''—as writtenThe tool to call, from the server's tool list.
inputModestringno'manual'manual, jsonas writtenForm (one field per argument) or JSON (one object).
argRowslist of objectsno[]—as writtenThe form's arguments. valueMode is fixed or expression; argType is the type the tool declared.
argRows[].idstringno''—as written—
argRows[].namestringno''—as written—
argRows[].valuestringno''—as written—
argRows[].valueModestringno'fixed'expression, fixedas written—
argRows[].argTypestringno'unknown'string, number, integer, boolean, object, array, unknownas written—
argRows[].requiredbooleannofalse—as written—
argsJsonstringno''—as writtenThe JSON arguments. Expressions are filled in before it is parsed.
timeoutSecondsintegerno301–300as writtenOverrides the server's own timeout for this step.
binaryHandlingstringno'file'file, base64as writtenStore images and audio as files, or keep the base64.
continueOnToolErrorbooleannofalse—as writtenTurn a tool-reported error into output instead of failing the node.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
structuredobject|nullStructured content (the tool’s own JSON)
textstringText content, parts joined
filesfile[]Image/audio parts stored as files
contentarrayEvery content part as the server sent it
toolNamestringTool called
serverIdstringMCP server id
serverNamestringMCP server name
isErrorbooleanThe tool reported an error and the run continued
errorMessagestring|nullThe 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)

workflows/mcp-tool-example.yaml
# 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: path and uploadFileExpression.
  • 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
protocolstringno'sftp'ftp, ftps, sftpnever carriedSFTP if the server offers it.
hoststringno''—never carriedThe server.
portintegerno00–65535never carried0 uses the protocol's default (22, 990, 21). For FTPS, 21 means explicit TLS.
usernamestringno''—never carriedThe login.
passwordstringno''—never carriedOnly a secret reference.
privateKeystringno''—never carriedSFTP key authentication.
passphrasestringno''—never carriedThe private key's passphrase.
hostKeyFingerprintstringno''—never carriedSFTP: the server's expected SHA256:… host-key fingerprint. Required.
operationstringno'list'list, download, upload, rename, deleteas writtenrename is Move.
pathstringno''—as writtenThe folder or file to act on. Expressions allowed.
recursivebooleannofalse—as writtenList: include subfolders.
uploadSourcestringno'file'file, textas writtenUpload: a file from the run, or text you write.
uploadFileExpressionstringno''—as writtenUpload: the file, such as {{ upstream.<nodeId>.output.file }}.
uploadTextstringno''—as writtenUpload: the text.
newPathstringno''—as writtenMove: the destination.
createDirectoriesbooleannofalse—as writtenUpload / Move: create missing folders on the way.
overwritebooleannofalse—as writtenMove: replace a destination that is already there. Off, an existing destination stops the node.
deleteFolderbooleannofalse—as writtenDelete: the path is a folder.
deleteRecursivebooleannofalse—as writtenDelete: also delete everything inside it.
connectTimeoutSecondsintegerno201–120as writtenBounds reaching the server.
transferTimeoutSecondsintegerno1201–900as writtenBounds one transfer or listing.

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
protocolstringProtocol ("ftp" | "ftps" | "sftp")
hoststringHost connected to
operationstringOperation
pathstringRemote path acted on
affectedPathsstring[]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)

workflows/ftp-example.yaml
# 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>.value with valueMode: expression.
  • Handles: one output (see Output below).
  • In a Package: app, version, resource, operation and inputs travel. The account travels as connection, a credential Slot id, as for the App trigger. Add the app's provider to compliance.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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
appstringno''—as writtenThe app: google-calendar, google-contacts, google-sheets, google-drive or google-gmail.
versionstringno''—as writtenThe app definition's version the step was built against.
resourcestringno''—as writtenWhat it acts on — an event, a contact, a range, a file. Each app's page lists them.
operationstringno''—as writtenWhat it does to it. Each app's page lists them, with their inputs.
connection (stored connectionId)stringno''—as connection: a credential Slot id, or nullThe connected account, from Connections.
inputsmap of name → objectno{}—as writtenOne entry per input of the operation: { value, valueMode }, where valueMode is fixed or expression.
inputs.<name>.valuestringno''—as written—
inputs.<name>.valueModestringno'fixed'expression, fixedas written—

Output

Read as {{ upstream.<node id>.output.<path> }} (an object):

PathTypeWhat it is
statusnumberHTTP status the app answered with
itemCountnumberItems returned (a list operation)
pagesnumberPages fetched
truncatedbooleanThe page cap stopped the follow
appstringApp
resourcestringResource
operationstringOperation

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)

workflows/app-example.yaml
# 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: inputExpression and each of fileExpressions.
  • 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 tableInputs entry names its table as table: dataTables.<name> (tableId is never in the file). The code is scanned by validate and build, 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[].table naming an undeclared Data Table (UNRESOLVED_LOCAL_REFERENCE).

Fields (from CodeNodeDataSchema; each row is a key of the node's data)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
languagestringno'python'pythonas writtenPython 3.12. The field exists so other languages can be added later; Python is the only one today.
codestringno''—as writtenYour code. It must define main(input, files).
inputExpressionstringno''—as writtenThe value main receives as input. A single {{ }} keeps its type (a list stays a list); literal JSON is parsed. Empty is None.
fileExpressionslist of stringsno[]at most 10 entriesas writtenUp to 10. Each resolves to a file reference (an FTP download, an email attachment, a Convert to File output) or a list of them.
tableInputslist of objectsno[]at most 5 entriesconverted per entry (see notes)Up to 5 Data Tables read as files (see Reading a Data Table). Each entry is { name, tableId, where? }.
tableInputs[].namestringyes——converted per entry (see notes)—
tableInputs[].tableIdstringyes——converted per entry (see notes)—
tableInputs[].whereobjectno——converted per entry (see notes)—
tableInputs[].where.matchstringno'all'any, allconverted per entry (see notes)—
tableInputs[].where.conditionslist of objectsyes—at most 50 entriesconverted per entry (see notes)—
tableInputs[].where.conditions[].columnstringyes——converted per entry (see notes)—
tableInputs[].where.conditions[].operatorstringyes—eq, neq, gt, gte, lt, lte, isEmpty, isNotEmpty, contains, startsWithconverted per entry (see notes)—
tableInputs[].where.conditions[].valueanyno——converted per entry (see notes)—
timeoutSecondsintegerno301–120as writtenThe 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):

PathTypeWhat it is
resultjsonWhat main() returned
filesfile[]Files written to /workspace/out/
files.0fileFirst output file
stdoutstringPrinted output (truncated)
durationMsnumberRun time (ms)
memoryBytesnumberPeak 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)

workflows/code-example.yaml
# 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: text travels. 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)

FieldTypeRequiredDefaultAllowed valuesIn the fileDescription
textstringno''—as writtenThe note's text.

Output

Nothing a later node reads.

Example (npx @elizon/cli example workflow --node note prints it with its manifest)

workflows/note-example.yaml
# 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: []