Workflow Definition

Workflow Definition Reference

A workflow definition is a JSON document describing a graph of steps and connections. It is passed as a JSON-encoded string in the definitionJson field when saving via the API.

Envelope

{
  "kind": "channelos.workflow",
  "schemaVersion": "1.0",
  "workflowId": "",
  "name": "my_workflow",
  "description": "Optional description",
  "graph": {
    "nodes": {},
    "edges": {}
  }
}
Field Description
kind Always "channelos.workflow"
schemaVersion Always "1.0"
workflowId Empty string for new workflows; returned by the server on save
name Unique name within the namespace
graph Node and edge graph — see below

Graph structure

The graph contains two maps: nodes and edges. Keys are arbitrary unique IDs.

Node object

"my_step": {
  "id": "my_step",
  "label": "My step",
  "actionType": "integration.http_request",
  "config": {
    "url": "https://api.example.com/notify",
    "method": "POST"
  },
  "position": { "x": 260, "y": 40, "width": 160, "height": 68 }
}
Field Description
id Unique node ID. Used in edge definitions and expressions
label Display label (shown in flow editor)
actionType Action type — see all types below
config Action-specific config — see per-action docs
position Layout coords (managed by flow editor; optional for programmatic use)

Edge object

"e1": {
  "id": "e1",
  "source": { "nodeId": "start", "portId": "out" },
  "target": { "nodeId": "notify", "portId": "in" }
}
Field Description
source.portId Common values: out, output, true, false, else (switch fallback), case value
target.portId Usually in or input
type normal (default), fork (parallel branch), join (parallel merge)

Data flow between steps

Every step receives a single JSON object called input. That object is the output of the previous step — or the workflow's start payload for the first step. Most steps pass their output forward unchanged; steps that explicitly produce new data replace the flowing payload.

Passthrough steps

These steps do not modify the payload. The next step receives exactly the same input as the passthrough step did:

Steps that produce new data

The following steps replace the flowing payload with their own output. Everything after them sees the new shape.

Step What the next step receives as input
data.transform The resolved mapping object
data.compute The computed return value
data.build_record The assembled key-value object
integration.http_request { "status": 200, "body": { ... } }
communication.send_message { "sent": true, "channel": "...", "to": "..." }
task.manual Whatever JSON the human submits when completing the task
signal.wait The signal payload sent via the runtime API
logic.join Merged object of all branch outputs — see below
logic.foreach_join { "results": [ iter1Output, iter2Output, ... ] }

Parallel branches and logic.join output

When branches converge at a logic.join, their outputs are merged into a single object. The key for each branch is taken from the label of the incoming edge. Give every edge from logic.parallel to the body a distinct label; the join output will mirror those labels:

// edge from parallel → fetch_employees  has label "employees"
// edge from parallel → fetch_rules      has label "rules"

// input to the node after join:
{
  "employees": { /* fetch_employees output */ },
  "rules":     { /* fetch_rules output */ }
}

If any incoming edge has no label, the branches are merged positionally as { "branches": [ branch0, branch1, ... ] }. Always label your parallel edges to get predictable named keys.

Foreach loop — item access

Inside a logic.foreach loop body, the current iteration item is always available at input.item. The original array container is also accessible as a sibling at its collection path:

// foreach config: { "collection": "recipients" }
// input.item     → current recipient object
// input.recipients → the full recipients array (still accessible)

// example in a send_message step inside the loop:
"to": "{{input.item.email}}"

Referencing earlier steps

Any step processed by the TypeScript runtime (data / integration / communication / task) records its output in the step store. You can reference those outputs from any later step using {{steps.stepId.field}}:

// step id "fetch_user" returned { "name": "Alice", "plan": "pro" }
"subject": "Hello {{steps.fetch_user.name}}"

// nested fields:
"body": "Plan: {{steps.fetch_user.plan}}"

Note: Engine-side steps (flow.*, logic.*, timing.*, signal.*) are not tracked in the step store and cannot be referenced via {{steps.*}}. To access the original workflow start payload at any point in the flow, use {{instance.input.field}}. To preserve other intermediate values across branches or after a join, use data.transform as a capture step.

Edge portId — when it matters

For most steps, source.portId is only used by the visual editor for layout and can be any string. It has routing significance for exactly two step types:

Step Required source.portId values
logic.condition "true" or "false"
logic.switch The literal string the expression evaluates to, plus "else" for the default branch

target.portId is always ignored at runtime.

Expressions

Runtime actions (data / integration / communication / task) support {{...}} expressions inside string config values. Engine actions (logic / flow / timing / signal) use direct input.field syntax without braces.

Expression Description
{{input.field}} Field from the current step's input payload
{{instance.input.field}} Field from the original workflow start payload — constant for the entire instance lifetime
{{steps.step_id.field}} Output field from a completed earlier step
{{env.VAR_NAME}} Secret / env var from the namespace secrets store
{{now}} ISO 8601 timestamp at evaluation time
{{today}} Current date as YYYY-MM-DD

Expressions can appear inside any string config value: "body": "Order {{input.orderId}} confirmed"

Engine actions

These run in the deterministic C++ workflow engine. Expression syntax: input.field (no {{ }}).

flow.start

Entry point. Every workflow must have exactly one. No config.

{ "actionType": "flow.start", "config": {} }

flow.end

Terminal node. The instance completes when reached. No config.

{ "actionType": "flow.end", "config": {} }

flow.subprocess

Start a child workflow and wait for it to complete. The parent instance parks until the child reaches flow.end, then resumes with the child's final output. (Previously named flow.run.)

{
  "actionType": "flow.subprocess",
  "config": {
    "workflowId": "2",
    "version": "deployed",
    "inputJson": "{}"
  }
}
Field Required Description
workflowId yes Numeric ID of the target workflow
version no "deployed" uses the currently deployed version
inputJson no Static JSON string passed to the spawned workflow

logic.condition

Branch on a boolean expression. Engine syntax — no {{ }} wrappers.

{
  "actionType": "logic.condition",
  "config": { "expression": "input.amount > 1000" }
}
Field Required Description
expression yes Boolean expression. Engine dot-path syntax: input.field. Supports arithmetic and comparison operators

Output ports: true, false

logic.switch

Route to one of N branches based on the value of an expression. Connect outgoing edges with portId equal to the expected case value. Use "else" port for the no-match case.

{
  "actionType": "logic.switch",
  "config": { "expression": "input.status" }
}
Field Required Description
expression yes Expression whose result is matched against outgoing edge portId values. Unmatched cases route through the "else" port

logic.parallel

Fork into multiple concurrent branches simultaneously. Connect outgoing edges with type: "fork". Pair with logic.join. No config.

{ "actionType": "logic.parallel", "config": {} }

logic.join

Wait for all parallel branches to arrive before continuing. Incoming edges use type: "join". No config.

{ "actionType": "logic.join", "config": {} }

logic.foreach

Iterate over an array. Runs the loop body once per item. The current item is always available as input.item; the full parent payload (including the source array) remains accessible at input.*.

{
  "actionType": "logic.foreach",
  "config": { "collection": "items" }
}
Field Required Description
collection no Dot-path to the array within the step input. Default: "items". The array itself remains accessible at input.<collection> inside the loop body

Inside the loop body: input.item is the current element (e.g. {{input.item.email}}). The source array is also still readable (e.g. {{input.items}} if collection is "items").

Output port: out — connect to the first node in the loop body. Pair with logic.foreach_join.

logic.foreach_join

Marks the end of the loop body. Triggers the next iteration, or exits when all items are done. No config.

{ "actionType": "logic.foreach_join", "config": {} }

Output: { "results": [...] } — array of each iteration body's final output. Output port: out.

timing.delay

Pause the workflow for a fixed duration in milliseconds. No threads are held; the instance is parked in the engine.

{
  "actionType": "timing.delay",
  "config": { "duration_ms": 3600000 }
}
Field Required Description
duration_ms yes Pause duration in milliseconds. The instance is parked; no threads are held

Common values: 5000 (5s)  |  60000 (1m)  |  3600000 (1h)  |  86400000 (24h)

timing.cron

Schedule periodic execution. The instance parks until the next cron tick fires, then completes. The engine automatically restarts the workflow on the next tick. Must be placed as the first node after flow.start.

{
  "actionType": "timing.cron",
  "config": {
    "cron": "0 9 * * MON-FRI",
    "timezone": "Europe/Kyiv"
  }
}
Field Required Description
cron yes 5-field expression: minute hour day month weekday
timezone no IANA timezone name (display only — engine schedules in UTC)
inputJson no Static JSON passed as input on each tick

signal.wait

Suspend the instance until a named external signal is received via POST /api/v1/runtime/signal. Signal payload becomes this step's output.

{
  "actionType": "signal.wait",
  "config": { "signal": "approved" }
}
Field Required Description
signal no Name of the signal to wait for. Defaults to the step ID if omitted

If signal is omitted, the step ID is used as the signal name. Output port: out.

signal.ask

Send a prompt message, park the instance until a signal arrives, then validate the received value. Retries automatically with an error message until validation passes.

{
  "actionType": "signal.ask",
  "config": {
    "prompt": "Enter your date of birth (YYYY-MM-DD):",
    "errorPrompt": "Invalid date, please try again:",
    "validate": "date",
    "channel": "telegram",
    "to": "{{instance.input.chatId}}"
  }
}
Field Required Description
prompt yes Message text sent to the user as the question.
errorPrompt no Sent when validation fails. Defaults to re-sending the prompt.
signal no Signal to wait for. Default: "user_input".
validate no nonempty | number | date | none. Default: none.
channel no Messaging channel, e.g. "telegram".
to no Recipient address or chat ID. Defaults to session chat.
valueKey no Key to extract from the signal payload. Default: "text".

Output: the validated value as { "value": <input> }.

signal.send_to_session

Send a named signal to the primary workflow instance associated with a session. Useful in sub-workflows to wake the parent after completing an operation.

{
  "actionType": "signal.send_to_session",
  "config": {
    "sessionId": "{{instance.input.sessionId}}",
    "signal": "operation_result"
  }
}
Field Required Description
sessionId yes Session ID used to look up the primary instance to signal.
signal yes Name of the signal to emit on the target instance.

The current step input is passed as the signal payload. Output port: out.

Data transformation

These run in the TypeScript runtime. Config string fields support {{...}} expressions.

data.transform

Reshape the payload. Each value in the mapping object is resolved as an expression.

{
  "actionType": "data.transform",
  "config": {
    "mapping": {
      "fullName": "{{input.firstName}} {{input.lastName}}",
      "email": "{{input.email}}",
      "total": "{{steps.calc.total}}"
    }
  }
}
Field Required Description
mapping yes Object where each value is a {{...}} expression. Non-expression values are passed through as-is

Output: the resolved mapping object.

data.compute

Evaluate a JavaScript expression in a sandboxed VM. Write raw JS — no {{ }}.

{
  "actionType": "data.compute",
  "config": { "expression": "input.qty * input.price * 1.2" }
}
Field Required Description
expression yes Raw JavaScript expression. No {{ }} wrappers. Return value becomes the step output

Available: input, steps, Math, Date, JSON, Array, Object, standard type constructors. Output: computed value.

data.build_record

Assemble a new object from named key-value pairs.

{
  "actionType": "data.build_record",
  "config": {
    "fields": [
      { "key": "name", "value": "{{input.name}}" },
      { "key": "label", "value": "Order {{input.orderId}}" }
    ]
  }
}
Field Required Description
fields yes Array of { key, value } objects. Values support {{...}} expressions. Output is an object with the assembled key-value pairs

data.render_template

Render a multi-line Handlebars template. The step's input is the template data context.

{
  "actionType": "data.render_template",
  "config": {
    "template": "Hello {{name}},\n\nYour balance is {{balance}} USD.",
    "outputKey": "text"
  }
}
Field Required Description
template yes Handlebars source. References input fields directly: {{fieldName}}
outputKey no Wrap output under this key. Default: "text". Set to "" for raw string

Special helpers: {{today}} (YYYY-MM-DD), {{now}} (ISO datetime)

data.format_text

Render a single-line Mustache-style template. References input fields via {{field}}.

{
  "actionType": "data.format_text",
  "config": {
    "template": "Hello {{name}}, your report for {{today}} is ready.",
    "outputKey": "text"
  }
}
Field Required Description
template yes Mustache template string. References step input fields via {{field}}
outputKey no Wrap the rendered string under this key. Default: "text". Set to "" for raw string

data.filter_list

Filter an array to items that satisfy a condition.

{
  "actionType": "data.filter_list",
  "config": {
    "source": "orders",
    "field": "status",
    "operator": "equals",
    "value": "pending",
    "caseSensitive": false
  }
}
Field Required Description
source no Dot-path to the array inside input
field no Field inside each item to test
operator yes equals | not_equals | contains | not_contains | starts_with | ends_with | is_empty | is_not_empty | greater_than | less_than
value no Comparison value
caseSensitive no Default: false

Output: { "items": [...] }

data.sort_list

Sort an array by a field.

{
  "actionType": "data.sort_list",
  "config": {
    "source": "orders",
    "field": "createdAt",
    "direction": "desc",
    "type": "date"
  }
}
Field Required Description
source no Dot-path to the array
field no Field to sort by
direction no asc (default) | desc
type no string (default) | number | date

data.merge_records

Shallow-merge two or more objects. Later sources overwrite earlier on key conflicts.

{
  "actionType": "data.merge_records",
  "config": {
    "sources": ["input", "steps.defaults", "steps.patch.extra"]
  }
}
Field Required Description
sources yes Array of dot-paths to merge in order. Later entries overwrite earlier on key conflicts

"input" = step input; "steps.stepId" = full step output; "steps.stepId.field" = nested field.

data.json_path

Extract a value using a JSONPath expression evaluated against the step's input.

{
  "actionType": "data.json_path",
  "config": {
    "expression": "$.data.items[0].id",
    "outputKey": "result"
  }
}
Field Required Description
expression yes JSONPath expression evaluated against the step's input
outputKey no Wrap result under this key. Default: "result"

Single match returns value directly; multi-match returns array; no match returns null.

data.extract_columns

Extract specific columns from a 2-D array (e.g. Google Sheets values). Column letters are Excel-style: "A", "B", etc.

{
  "actionType": "data.extract_columns",
  "config": {
    "source": "values",
    "skipRows": 1,
    "columns": ["A", "C", "E"],
    "keepEmpty": false
  }
}
Field Required Description
source no Dot-path to the 2D array in input. Default: "values"
skipRows no Number of header rows to skip. Default: 0
columns yes Excel-style column letters to extract, e.g. ["A", "C", "E"]
keepEmpty no Include rows where all selected cells are empty. Default: false

Output: { "items": [[...], [...], ...] } — each item is an array of cell values in column order.

data.summarize

Wrap a list into a summary document with metadata. Typically used after logic.foreach_join.

{
  "actionType": "data.summarize",
  "config": {
    "title": "Weekly Report",
    "source": "results",
    "includeDate": true
  }
}
Field Required Description
title no Report title included in output
source no Dot-path to the array in input. Default: "results"
includeDate no Include generatedAt and generatedDate in output. Default: true

Output: { "title", "generatedAt", "generatedDate", "totalCount", "items" }

data.array_to_map

Convert an array of objects into a keyed map.

{
  "actionType": "data.array_to_map",
  "config": {
    "source": "items",
    "keyField": "sku",
    "valueField": "price"
  }
}
Field Required Description
source no Dot-path to the array in input. Default: "items"
keyField yes Item field to use as the map key
valueField no Item field to use as the map value. Omit to use the full item object

Output: { "sku-001": 29.99, "sku-002": 49.99, ... }

data.file Files

Generate or fetch a binary artifact stored outside workflow state. Use artifactId in email attachments.

{
  "actionType": "data.file",
  "config": {
    "action": "generate",
    "name": "invoice.pdf",
    "mimeType": "application/pdf",
    "encoding": "base64",
    "data": "{{steps.render.pdfBase64}}"
  }
}
Field Required Description
action no "generate" (default) | "fetch"
name yes Artifact filename
encoding no "base64" (default) | "utf8" | "hex"
data yes (generate) Content encoded per encoding
url yes (fetch) HTTPS URL to download

Output: { "artifactId", "name", "mimeType", "size", "expiresAt" }

data.excel Files

Generate an .xlsx file either by filling a namespace template or from scratch. Returns an artifactId for use with communication.send_file.

// Fill a template
{
  "actionType": "data.excel",
  "config": {
    "action": "from_template",
    "templatePath": "templates/report.xlsx",
    "sheet": "Data",
    "startRow": 2,
    "rows": "{{steps.sql_report.rows}}",
    "columns": ["name", "date", "amount"],
    "name": "report_{{input.date}}.xlsx"
  }
}

// Generate from scratch
{
  "actionType": "data.excel",
  "config": {
    "action": "generate",
    "headers": ["Name", "Date", "Amount"],
    "rows": "{{steps.sql_report.rows}}",
    "columns": ["name", "date", "amount"],
    "name": "report.xlsx"
  }
}
Field Required Description
action no from_template (default) or generate.
templatePath from_template Path inside workspaces/ns_{id}/files/, e.g. "templates/report.xlsx".
sheet no Sheet name to write data into. Default: first sheet.
startRow no 1-based row index where data rows begin. Default: 2 (skips header row).
headers generate Array of column header strings for row 1 (generate mode only).
rows yes Array of row objects from a previous step, e.g. {{steps.sql_report.rows}}.
columns no Ordered list of object keys for column order, e.g. ["name","date","amount"].
name yes Output filename, e.g. "report_2026-06-07.xlsx".

Output: { "artifactId": "..." }. Pass artifactId to communication.send_file to deliver the file.

data.parse_csv Files

Parse a CSV string into an array of objects (with header row) or an array of arrays (without header row).

{
  "actionType": "data.parse_csv",
  "config": {
    "source": "csvContent",
    "delimiter": ",",
    "hasHeader": true,
    "outputKey": "rows"
  }
}
Field Required Description
source no Dot-path to the CSV string in the step input. Leave blank to use the input directly.
delimiter no Column delimiter character. Default: ",".
hasHeader no When true (default), first row is used as column names → array of objects. When false → array of arrays.
skipEmpty no Skip blank rows. Default: true.
maxRows no Error if row count exceeds this limit. Default: 10000.
outputKey no Key name for the result array in output. Default: "rows".

Output: { "rows": [...] } or { [outputKey]: [...] }.

data.to_csv Files

Convert an array of objects or arrays into a CSV string.

{
  "actionType": "data.to_csv",
  "config": {
    "source": "rows",
    "columns": ["name", "email", "amount"],
    "delimiter": ",",
    "includeHeader": true,
    "outputKey": "csv"
  }
}
Field Required Description
source no Dot-path to the array in the step input. Leave blank to use the input directly.
columns no Ordered list of object keys to include, e.g. ["name","email"]. Default: all keys from first row.
delimiter no Column delimiter character. Default: ",".
includeHeader no Write column names as first row. Default: true.
outputKey no Key name for the CSV string in output. Default: "csv".

Output: { "csv": "..." } or { [outputKey]: "..." }.

data.db_query Storage

Run a SELECT query against a database and return matching rows.

{
  "actionType": "data.db_query",
  "config": {
    "connectionSecret": "db/prod",
    "driver": "postgres",
    "table": "orders",
    "where": [["status", "=", "pending"]],
    "limit": 100
  }
}
Field Required Description
connectionSecret yes Secret name containing the DB connection string.
driver no sqlite (default) | postgres | mysql.
table yes Table name to query.
where no Array of [column, operator, value] conditions, e.g. [["status","=","active"]].
limit no Maximum rows to return.

Output: { "rows": [...] }.

data.db_insert Storage

Insert a single row into a database table.

{
  "actionType": "data.db_insert",
  "config": {
    "connectionSecret": "db/prod",
    "driver": "postgres",
    "table": "orders",
    "fields": [["status", "pending"], ["created_at", "{{now}}"]]
  }
}
Field Required Description
connectionSecret yes Secret name for the connection string.
driver no sqlite | postgres | mysql.
table yes Target table name.
fields yes Array of [column, value] pairs to insert.

Output: { "lastInsertRowid": ... }.

data.db_update Storage

Update rows in a database table matching the given conditions.

{
  "actionType": "data.db_update",
  "config": {
    "connectionSecret": "db/prod",
    "driver": "postgres",
    "table": "orders",
    "fields": [["status", "shipped"]],
    "where": [["id", "=", "{{input.orderId}}"]]
  }
}
Field Required Description
connectionSecret yes Secret name for the connection string.
driver no sqlite | postgres | mysql.
table yes Target table name.
fields yes Array of [column, value] pairs to set.
where no Array of [column, operator, value] filter conditions.

Output: { "changes": N } — number of affected rows.

data.db_delete Storage

Delete rows from a database table matching the given conditions.

{
  "actionType": "data.db_delete",
  "config": {
    "connectionSecret": "db/prod",
    "driver": "postgres",
    "table": "sessions",
    "where": [["expires_at", "<", "{{now}}"]]
  }
}
Field Required Description
connectionSecret yes Secret name for the connection string.
driver no sqlite | postgres | mysql.
table yes Target table name.
where no Array of [column, operator, value] filter conditions.

Output: { "changes": N } — number of deleted rows.

data.db_upsert Storage

Insert or update a row in a database table based on a conflict key.

{
  "actionType": "data.db_upsert",
  "config": {
    "connectionSecret": "db/prod",
    "driver": "sqlite",
    "table": "settings",
    "fields": [["key", "theme"], ["value", "dark"]],
    "conflictKey": "key"
  }
}
Field Required Description
connectionSecret yes Secret name for the connection string.
driver no sqlite | postgres | mysql.
table yes Target table name.
fields yes Array of [column, value] pairs.
conflictKey yes Column name used as the unique conflict key for the upsert.

Output: { "lastInsertRowid": ..., "changes": N }.

data.db_bulk_insert Storage

Insert multiple rows into a database table in a single transaction. Optionally creates the table if it does not exist.

{
  "actionType": "data.db_bulk_insert",
  "config": {
    "connectionSecret": "db/prod",
    "driver": "sqlite",
    "table": "report_data",
    "rows": "{{steps.parse_csv.rows}}",
    "createTableIfNotExists": true,
    "onConflict": "ignore"
  }
}
Field Required Description
connectionSecret yes Secret name for the connection string.
driver no sqlite | postgres | mysql.
table yes Target table name.
rows no Array of row objects or 2D arrays to insert.
rowsFormat no "" for objects (default) or "spreadsheet" for 2D arrays.
createTableIfNotExists no Infer columns from the first row and create the table if missing. Default: false.
recreateTable no Drop and recreate the table before insert. Default: false.
addIdColumn no Add an auto-increment id column when creating the table. Default: false.
onConflict no fail (default) | ignore | replace.
columnMap no Map input keys to column names, e.g. {"firstName":"first_name"}.

Output: { "insertedRows": N }.

Communication

data.namespace_store Storage

Persistent key-value store scoped to the namespace. Lets workflow runs share state across executions — useful for deduplication, counters, cross-workflow flags, and rolling accumulators. Requires base plan or above.

// set
{ "actionType": "data.namespace_store", "config": { "operation": "set", "key": "last_run_id", "value": "{{input.id}}", "ttlSeconds": 86400 } }

// get
{ "actionType": "data.namespace_store", "config": { "operation": "get", "key": "last_run_id" } }

// increment (atomic counter)
{ "actionType": "data.namespace_store", "config": { "operation": "increment", "key": "msg_count", "delta": 1, "outputKey": "count" } }
Field Required Description
operation yes get / set / delete / increment / exists
key yes Key string. Supports {{expression}} interpolation.
value set only Any JSON-serialisable value. Max 64 KB.
ttlSeconds no Expire after N seconds (for set and increment).
delta no Amount to add/subtract for increment. Default 1. Can be negative.
outputKey no Wrap result under this key instead of "value".

Output: { "value": <result> } or { [outputKey]: <result> }. get returns null when missing or expired.

communication.send_message

Send a message via email, Telegram, or WhatsApp.

// Email (plain text)
{
  "actionType": "communication.send_message",
  "config": {
    "channel": "email",
    "to": "{{input.email}}",
    "subject": "Your order is confirmed",
    "body": "Hello {{input.name}}, your order is ready."
  }
}

// Email (HTML + plain text fallback)
{
  "actionType": "communication.send_message",
  "config": {
    "channel": "email",
    "to": "{{input.email}}",
    "subject": "Your order is confirmed",
    "body": "Hello {{input.name}}, your order is ready.",
    "htmlBody": "{{steps.render_html.output.html}}"
  }
}

// Telegram
{
  "actionType": "communication.send_message",
  "config": {
    "channel": "telegram",
    "to": "{{env.TELEGRAM_CHAT_ID}}",
    "text": "New order: {{input.orderId}}"
  }
}

// WhatsApp
{
  "actionType": "communication.send_message",
  "config": {
    "channel": "whatsapp",
    "to": "{{input.phone}}",
    "text": "Hello {{input.name}}, your booking is confirmed."
  }
}
Channel Required fields Notes
email to, subject, body Optional: htmlBody, templateId, attachments: [{artifactId}]
telegram to, text Credentials from namespace secrets
whatsapp to, text Credentials from namespace secrets. Requires messaging plan.

When htmlBody is provided, a multipart/alternative email is sent — body is used as the plain-text fallback for clients that do not render HTML. Use data.render_template to generate htmlBody from a Handlebars template.

communication.ask_confirm Telegram

Send a message with Confirm and Cancel inline keyboard buttons, then park the workflow until the user taps one. Routes to confirmed or cancelled output ports accordingly.

{
  "actionType": "communication.ask_confirm",
  "config": {
    "prompt": "Are you sure you want to delete this record?",
    "confirmLabel": "Yes, delete",
    "cancelLabel": "No, keep it",
    "channel": "telegram"
  }
}
Field Required Description
prompt yes Message text shown to the user.
confirmLabel no Text on the confirm button. Default: “Confirm”.
cancelLabel no Text on the cancel button. Default: “Cancel”.
confirmValue no callback_data for the confirm button. Default: "confirm".
signal no Signal name to wait for. Default: "menu_choice".
to no Chat ID to send to. Defaults to the session chat.
channel no Delivery channel. Default: "telegram".

Output ports: confirmed and cancelled. Connect each to the desired branch.

communication.pick_from_list Telegram

Build a dynamic inline keyboard from an array in the step input, send it to the user, and park until one item is selected. The selected value is emitted on the output.

{
  "actionType": "communication.pick_from_list",
  "config": {
    "prompt": "Select an employee:",
    "itemsPath": "employees.items",
    "labelField": "name",
    "valueField": "id",
    "signal": "user_action",
    "channel": "telegram"
  }
}
Field Required Description
prompt yes Message text shown above the keyboard.
itemsPath yes Dot-path to the array in the step input, e.g. "employees.items".
labelField no Item field to display as button label. Default: "name".
valueField no Item field used as the selection value. Default: "id".
signal no Signal name to wait for. Default: "user_action".
to no Chat ID to send to. Defaults to session chat.
channel no Delivery channel. Default: "telegram".

Output: the selected item’s valueField value.

communication.send_file Telegram

Send an artifact file through a delivery channel. Typically used after data.excel or data.file to deliver a generated file to the user.

{
  "actionType": "communication.send_file",
  "config": {
    "channel": "telegram",
    "artifactId": "{{steps.generate_report.artifactId}}",
    "caption": "Your monthly report is attached.",
    "to": "{{instance.input.chatId}}"
  }
}
Field Required Description
channel yes Delivery channel. Currently: telegram.
artifactId yes ID of the artifact to send, e.g. {{steps.generate_report.artifactId}}.
to no Telegram chat ID. Leave empty to send to the session chat.
caption no Optional file caption (max 1024 characters).
keyboard_buttons no Inline keyboard as 2D array: [[{"text":"OK","callback_data":"ok"}]].
botSecret no Secrets store key for the bot token. Use for one-way notifications without a user session.

Output port: sent.

telegram.register_session Telegram

Transfer the Telegram session (chatId → instanceId mapping) to the current workflow instance. Use as the first step in sub-workflows launched via flow.run or flow.subprocess so that communication.send_message and reply routing work correctly.

{
  "actionType": "telegram.register_session",
  "config": {
    "chatId": "{{input.chatId}}"
  }
}
Field Required Description
chatId yes Telegram chat ID to register for this instance. Typically "{{input.chatId}}" when the parent passed chatId through the flow.run payload.

Integrations

integration.http_request

Make an outbound HTTP request. Step input is optionally sent as the JSON body. Supports static Bearer tokens and full OAuth 2.0 client credentials / refresh token flows with automatic token caching.

// Bearer auth
{
  "actionType": "integration.http_request",
  "config": {
    "method": "POST",
    "url": "https://api.example.com/orders",
    "sendBody": true,
    "authType": "bearer",
    "authBearerSecret": "my_api_key"
  }
}

// OAuth 2.0 — client credentials
{
  "actionType": "integration.http_request",
  "config": {
    "method": "GET",
    "url": "https://api.salesforce.com/services/data/v59.0/query",
    "authType": "oauth2",
    "authGrantType": "client_credentials",
    "authTokenUrl": "https://login.salesforce.com/services/oauth2/token",
    "authClientIdSecret": "sf/client_id",
    "authClientSecretSecret": "sf/client_secret",
    "authScope": "api",
    "authCacheKey": "salesforce_prod"
  }
}

Common fields

Field Required Description
method yes "GET" | "POST" | "PUT" | "DELETE". Default: "POST"
url yes Target URL. Supports {{...}} expressions
timeoutMs no Abort after this many milliseconds. Default: 30000. Range: 100–120 000
sendBody no Send step input as JSON body (ignored for GET / DELETE). Default: true
authType no "none" (default) | "bearer" | "oauth2"

Bearer auth — authType: "bearer"

Field Required Description
authBearerSecret yes SecretsStore key whose value is the static Bearer token

OAuth 2.0 — authType: "oauth2"

Field Required Description
authGrantType no "client_credentials" (default) | "refresh_token"
authTokenUrl yes OAuth 2.0 token endpoint URL
authClientIdSecret yes SecretsStore key for the OAuth client_id
authClientSecretSecret yes SecretsStore key for the OAuth client_secret
authRefreshTokenSecret if refresh_token SecretsStore key for the refresh token (refresh_token grant only)
authScope no Space-separated OAuth scopes to request
authCacheKey no Key for caching the token across executions. Omit to always fetch fresh
authExtraParams no Extra form fields for the token request, e.g. {"audience": "https://api.example.com"}

Output: { "status": 200, "body": { ... } }. Non-2xx responses fail the step (retryable on 5xx, 408, 429).

integration.google_sheets Google Sheets

Read from or write to a Google Sheets spreadsheet via a service account.

{
  "actionType": "integration.google_sheets",
  "config": {
    "credentialSecret": "gsheets_sa_key",
    "operation": "read",
    "spreadsheetId": "{{env.SHEET_ID}}",
    "range": "Sheet1!A1:D100"
  }
}
Field Required Description
credentialSecret yes Secret name containing the service account JSON key
operation yes read | append | update | clear | batchUpdate
spreadsheetId yes Google Sheets document ID
range yes A1 notation range, e.g. Sheet1!A1:D100

integration.script_runner

Execute a script file from the namespace script store. Input is passed via stdin as JSON; stdout is captured as output.

Owner only — this action type is available exclusively to namespace owners. Workflows using it cannot be created or edited by regular users.

{
  "actionType": "integration.script_runner",
  "config": {
    "interpreter": "python",
    "scriptPath": "transform_data.py"
  }
}
Field Required Description
interpreter yes "python" | "node" | "bash"
scriptPath yes Path to the script in the namespace script store

Input is passed to the script via stdin as JSON; captured stdout becomes the step output.

integration.sql External SQL

Execute a raw SQL statement against a PostgreSQL, MySQL, or SQLite database. Supports SELECT queries, DML (INSERT/UPDATE/DELETE), and multi-statement DDL.

// Query (SELECT)
{
  "actionType": "integration.sql",
  "config": {
    "connectionSecret": "db/prod",
    "driver": "postgres",
    "operation": "query",
    "sql": "SELECT id, name, amount FROM orders WHERE status = $1",
    "params": ["pending"],
    "outputKey": "rows"
  }
}

// Execute (INSERT/UPDATE/DELETE)
{
  "actionType": "integration.sql",
  "config": {
    "connectionSecret": "db/prod",
    "driver": "postgres",
    "operation": "execute",
    "sql": "UPDATE orders SET status = $1 WHERE id = $2",
    "params": ["shipped", "{{input.orderId}}"]
  }
}
Field Required Description
connectionSecret yes Secret name containing the DB connection string.
driver no sqlite (default) | postgres | mysql.
operation yes query (SELECT) | execute (INSERT/UPDATE/DELETE) | exec (multi-statement DDL).
sql yes SQL statement to execute. Use $1, $2, … for parameterized values.
params no Array of positional parameter values, e.g. ["active", 42].
outputKey no Key name for results in output. Default: "rows" for query.
timeout no Query timeout in milliseconds.

Output for query: { "rows": [...] }. For execute: { "changes": N }.

Finance Finance

Finance actions manage invoices and payments within the built-in billing system. All monetary values are in cents (minor units).

finance.create_invoice

Create a new invoice for a client.

{
  "actionType": "finance.create_invoice",
  "config": {
    "clientId": "{{input.clientId}}",
    "amount": "4900",
    "currency": "usd",
    "items": "[{\"description\":\"Base plan (monthly)\",\"amount\":4900}]",
    "dueDate": "2026-07-01"
  }
}
Config field Required Description
clientId yes Target client ID (number)
amount yes Invoice total in cents
currency no ISO currency code (default: USD)
items yes JSON array of {description, amount} objects
dueDate no Due date (YYYY-MM-DD)

Output: { "invoiceId": N, "status": "pending" }

finance.charge

Initiate payment for an invoice. Returns a checkout URL (if external provider) or marks the payment as pending.

{
  "actionType": "finance.charge",
  "config": {
    "invoiceId": "{{steps.create_inv.invoiceId}}",
    "returnUrl": "https://portal.example.com/thank-you"
  }
}
Config field Required Description
invoiceId yes Invoice to charge
returnUrl no Redirect URL after checkout

Output: { "paymentId": N, "checkoutUrl": "https://..." }

finance.send_invoice

Send invoice notification via email or Telegram.

{
  "actionType": "finance.send_invoice",
  "config": {
    "invoiceId": "{{steps.create_inv.invoiceId}}",
    "channel": "email",
    "to": "{{input.email}}"
  }
}
Config field Required Description
invoiceId yes Invoice to send
channel yes email or telegram
to yes Recipient address or chatId

Output: { "sent": true }

finance.check_payment_status

Get the current status of a payment.

{
  "actionType": "finance.check_payment_status",
  "config": {
    "paymentId": "{{steps.charge.paymentId}}"
  }
}
Config field Required Description
paymentId yes Payment to check

Output: { "status": "completed" | "pending" | "failed", "paidAt": "..." | null }

finance.refund

Refund a completed payment. Omit amount for a full refund.

{
  "actionType": "finance.refund",
  "config": {
    "paymentId": "{{steps.charge.paymentId}}",
    "amount": "2500"
  }
}
Config field Required Description
paymentId yes Payment to refund
amount no Partial refund in cents (omit for full)

Output: { "refunded": true, "amount": 2500 }

Other

debug.log

Write a log entry and pass input through unchanged. Safe to insert anywhere for debugging.

{
  "actionType": "debug.log",
  "config": {
    "message": "Current amount: {{input.amount}}",
    "level": "info"
  }
}
Field Required Description
message no Log message text. Supports {{...}} expressions
level no "debug" | "info" (default) | "warn" | "error"

level: debug | info (default) | warn | error

task.manual

Pause the workflow and wait for a human to complete the task. The instance stays in running state until an external caller invokes POST /api/v1/runtime/step/complete.

{ "actionType": "task.manual", "config": {} }

No configuration required. Complete the task via:

POST /api/v1/runtime/step/complete
{
  "instanceId": "42",
  "stepId": "review_task",
  "outputJson": "{\"approved\": true, \"comment\": \"Looks good\"}"
}

The outputJson becomes this step's output: {{steps.review_task.approved}} in subsequent steps. Output port: output.

The action library is actively growing. New action types are added regularly — contact us if you need an integration or capability not listed here.

Examples

Complete workflow definitions ready to copy and use. Pass the JSON as the definitionJson string value when saving.

Minimal workflow — Start → HTTP POST → End
{
  "kind": "channelos.workflow",
  "schemaVersion": "1.0",
  "workflowId": "",
  "name": "minimal_example",
  "graph": {
    "nodes": {
      "start": { "id": "start", "label": "Start", "actionType": "flow.start", "config": {} },
      "call": {
        "id": "call",
        "label": "Call API",
        "actionType": "integration.http_request",
        "config": {
          "method": "POST",
          "url": "https://httpbin.org/post",
          "sendBody": true
        }
      },
      "end": { "id": "end", "label": "End", "actionType": "flow.end", "config": {} }
    },
    "edges": {
      "e1": { "id": "e1", "source": { "nodeId": "start", "portId": "out" }, "target": { "nodeId": "call", "portId": "in" } },
      "e2": { "id": "e2", "source": { "nodeId": "call", "portId": "out" }, "target": { "nodeId": "end", "portId": "in" } }
    }
  }
}

Start input: any JSON — the whole payload is forwarded as the HTTP POST body. Example: { "orderId": "ord-001", "amount": 250 }

Conditional branch — Route high-value orders to a priority handler
{
  "kind": "channelos.workflow",
  "schemaVersion": "1.0",
  "workflowId": "",
  "name": "conditional_branch",
  "graph": {
    "nodes": {
      "start": { "id": "start", "label": "Start", "actionType": "flow.start", "config": {} },
      "check": {
        "id": "check",
        "label": "High value?",
        "actionType": "logic.condition",
        "config": { "expression": "input.amount > 1000" }
      },
      "priority": {
        "id": "priority",
        "label": "Notify priority",
        "actionType": "communication.send_message",
        "config": {
          "channel": "telegram",
          "to": "{{env.TELEGRAM_CHAT_ID}}",
          "text": "High-value order: {{input.orderId}} ({{input.amount}} USD)"
        }
      },
      "standard": {
        "id": "standard",
        "label": "Standard notify",
        "actionType": "communication.send_message",
        "config": {
          "channel": "email",
          "to": "{{input.email}}",
          "subject": "Order received",
          "body": "Your order {{input.orderId}} is being processed."
        }
      },
      "end": { "id": "end", "label": "End", "actionType": "flow.end", "config": {} }
    },
    "edges": {
      "e1": { "id": "e1", "source": { "nodeId": "start", "portId": "out" }, "target": { "nodeId": "check", "portId": "in" } },
      "e2": { "id": "e2", "source": { "nodeId": "check", "portId": "true" }, "target": { "nodeId": "priority", "portId": "in" } },
      "e3": { "id": "e3", "source": { "nodeId": "check", "portId": "false" }, "target": { "nodeId": "standard", "portId": "in" } },
      "e4": { "id": "e4", "source": { "nodeId": "priority", "portId": "out" }, "target": { "nodeId": "end", "portId": "in" } },
      "e5": { "id": "e5", "source": { "nodeId": "standard", "portId": "out" }, "target": { "nodeId": "end", "portId": "in" } }
    }
  }
}

Start input: { "orderId": "ord-042", "amount": 1500, "email": "[email protected]" }. Amount > 1000 routes to the Telegram priority path; amount ≤ 1000 sends email.

Foreach loop — Send an email to each recipient in a list
{
  "kind": "channelos.workflow",
  "schemaVersion": "1.0",
  "workflowId": "",
  "name": "foreach_email",
  "graph": {
    "nodes": {
      "start": { "id": "start", "label": "Start", "actionType": "flow.start", "config": {} },
      "loop": {
        "id": "loop",
        "label": "For each recipient",
        "actionType": "logic.foreach",
        "config": { "collection": "recipients" }
      },
      "send": {
        "id": "send",
        "label": "Send email",
        "actionType": "communication.send_message",
        "config": {
          "channel": "email",
          "to": "{{input.item.email}}",
          "subject": "{{input.item.subject}}",
          "body": "Hello {{input.item.name}}, {{input.item.message}}"
        }
      },
      "loop_end": { "id": "loop_end", "label": "Loop end", "actionType": "logic.foreach_join", "config": {} },
      "end": { "id": "end", "label": "End", "actionType": "flow.end", "config": {} }
    },
    "edges": {
      "e1": { "id": "e1", "source": { "nodeId": "start", "portId": "out" }, "target": { "nodeId": "loop", "portId": "in" } },
      "e2": { "id": "e2", "source": { "nodeId": "loop", "portId": "out" }, "target": { "nodeId": "send", "portId": "in" } },
      "e3": { "id": "e3", "source": { "nodeId": "send", "portId": "out" }, "target": { "nodeId": "loop_end", "portId": "in" } },
      "e4": { "id": "e4", "source": { "nodeId": "loop_end", "portId": "out" }, "target": { "nodeId": "end", "portId": "in" } }
    }
  }
}

Input: { "recipients": [{ "email": "...", "name": "...", "subject": "...", "message": "..." }, ...] }. Each iteration exposes the current element as input.item.

Approval flow — Human review with approve/reject branch
{
  "kind": "channelos.workflow",
  "schemaVersion": "1.0",
  "workflowId": "",
  "name": "approval_flow",
  "graph": {
    "nodes": {
      "start": { "id": "start", "label": "Start", "actionType": "flow.start", "config": {} },
      "capture": {
        "id": "capture",
        "label": "Capture input",
        "actionType": "data.transform",
        "config": {
          "mapping": {
            "requesterEmail": "{{input.requesterEmail}}",
            "title": "{{input.title}}"
          }
        }
      },
      "notify_reviewer": {
        "id": "notify_reviewer",
        "label": "Notify reviewer",
        "actionType": "communication.send_message",
        "config": {
          "channel": "email",
          "to": "{{env.REVIEWER_EMAIL}}",
          "subject": "Review requested: {{input.title}}",
          "body": "Please review the request and complete the task in the portal."
        }
      },
      "review": {
        "id": "review",
        "label": "Human review",
        "actionType": "task.manual",
        "config": {}
      },
      "check": {
        "id": "check",
        "label": "Approved?",
        "actionType": "logic.condition",
        "config": { "expression": "input.approved === true" }
      },
      "on_approved": {
        "id": "on_approved",
        "label": "Notify approved",
        "actionType": "communication.send_message",
        "config": {
          "channel": "email",
          "to": "{{steps.capture.requesterEmail}}",
          "subject": "Your request was approved",
          "body": "Your request has been approved."
        }
      },
      "on_rejected": {
        "id": "on_rejected",
        "label": "Notify rejected",
        "actionType": "communication.send_message",
        "config": {
          "channel": "email",
          "to": "{{steps.capture.requesterEmail}}",
          "subject": "Your request was rejected",
          "body": "Your request was not approved."
        }
      },
      "end": { "id": "end", "label": "End", "actionType": "flow.end", "config": {} }
    },
    "edges": {
      "e1": { "id": "e1", "source": { "nodeId": "start", "portId": "out" }, "target": { "nodeId": "capture", "portId": "in" } },
      "e2": { "id": "e2", "source": { "nodeId": "capture", "portId": "out" }, "target": { "nodeId": "notify_reviewer", "portId": "in" } },
      "e3": { "id": "e3", "source": { "nodeId": "notify_reviewer", "portId": "out" }, "target": { "nodeId": "review", "portId": "in" } },
      "e4": { "id": "e4", "source": { "nodeId": "review", "portId": "output" }, "target": { "nodeId": "check", "portId": "in" } },
      "e5": { "id": "e5", "source": { "nodeId": "check", "portId": "true" }, "target": { "nodeId": "on_approved", "portId": "in" } },
      "e6": { "id": "e6", "source": { "nodeId": "check", "portId": "false" }, "target": { "nodeId": "on_rejected", "portId": "in" } },
      "e7": { "id": "e7", "source": { "nodeId": "on_approved", "portId": "out" }, "target": { "nodeId": "end", "portId": "in" } },
      "e8": { "id": "e8", "source": { "nodeId": "on_rejected", "portId": "out" }, "target": { "nodeId": "end", "portId": "in" } }
    }
  }
}

Start input: { "title": "Budget increase Q3", "requesterEmail": "[email protected]" }

Complete via API: POST /api/v1/runtime/step/complete with { "instanceId": "...", "stepId": "review", "outputJson": "{\"approved\": true}" }