Workflow Studio

Nodes Reference

Every workflow node type with all configuration properties, inputs, outputs, and error handling options.

Node Structure

Every node in a workflow shares a common base structure. Type-specific config lives in the config object.

FieldTypeRequiredDefaultDescription
idstringrequiredNode identifier. Unique within the workflow. Used in edges and to reference node output.
typeenumrequiredNode type: action · condition · loop · delay · transform · approval · http_request · set_variable · sub_workflow · log · error_handler · parallel · merge
namestringoptionalHuman-readable label shown on the canvas.
configobjectrequiredType-specific configuration object. See per-type docs below.
position{ x, y }optionalCanvas position for the visual editor. Ignored by the execution engine.
disabledbooleanoptionalfalseIf true, node is skipped during execution (outputs empty object). Useful for debugging.
retryPolicyobjectoptionalNode-level retry config. Overrides workflow-level. See retry policy schema.
timeoutintegeroptional30000Node execution timeout in milliseconds. Max 300000 (5 min). Throws on timeout.
onErrorenumoptionalthrowthrow (fail run) · continue (log and continue) · retry (use retryPolicy)
metaobjectoptionalArbitrary key-value metadata stored with the node. Not used at runtime.

Retry policy schema

FieldTypeRequiredDefaultDescription
maxAttemptsintegeroptional3Maximum number of attempts including the first.
strategyenumoptionalexponentialfixed · exponential · linear
delayMsintegeroptional1000Base delay in milliseconds between retries.
maxDelayMsintegeroptional60000Maximum delay cap for exponential strategy.
retryOnstring[]optionalError codes that trigger a retry. Empty = retry on any error.

action — Integration Action

Executes a pre-built integration action (email, Slack, Jira, HTTP, DB write, etc.).

FieldTypeRequiredDefaultDescription
actionstringrequiredDot-notation action ID: "email.send" · "slack.postMessage" · "jira.createIssue" · "db.createRecord" etc.
connectorIdstringoptionalFor actions that need a connector (e.g. jira, slack). References a configured connector ID.
inputobjectrequiredAction-specific input object. All values support {{template}} expressions.
outputMappingobjectoptionalRename action output keys: { myAlias: "originalKey" }. Makes output cleaner for downstream nodes.

Built-in actions (no connector required)

Action IDDescriptionKey inputs
email.sendSend a transactional emailto, subject, body, templateId, attachments
notification.sendSend in-app FlowOS notificationuserId, title, body, link
db.createRecordCreate a record in a DB tabletableSlug, fields
db.updateRecordUpdate a record by IDtableSlug, recordId, fields
db.deleteRecordDelete a record by IDtableSlug, recordId
db.queryRecordsQuery records and return resultstableSlug, filters, sort, limit
itsm.createIncidentCreate an ITSM incidenttitle, severity, description
itsm.resolveIncidentResolve an incidentincidentId, resolution, rootCause
itsm.addCommentAdd comment to ITSM recordentityType, entityId, body, type
catalog.submitRequestSubmit a service catalog requestcatalogItemId, requestedBy, fields
secret.getRead a vault secret valuename (returns value — never logged)
workflow.triggerTrigger another workflowworkflowId, input

condition — Branch

Evaluates a boolean expression and routes to one of two branches (true edge or false edge).

FieldTypeRequiredDefaultDescription
expressionstringrequiredA boolean expression using template syntax. e.g. "{{trigger.record.severity}} === 'P1'"
modeenumoptionalsimplesimple (single expression) · multi (array of named branches — acts as switch/case).
branchesobject[]optionalFor mode=multi: [{ id, label, expression }]. First matching branch wins.
defaultBranchstringoptionalBranch ID to take when no multi-branch condition matches.
json
// Simple condition
{
  "id": "check-severity",
  "type": "condition",
  "config": {
    "expression": "{{trigger.record.severity}} === 'P1' || {{trigger.record.severity}} === 'P2'"
  }
}
// Edges: from check-severity (true) → pager-alert
//         from check-severity (false) → slack-notify

// Multi-branch (switch)
{
  "id": "route-by-category",
  "type": "condition",
  "config": {
    "mode": "multi",
    "branches": [
      { "id": "hardware",  "label": "Hardware",  "expression": "{{trigger.record.category}} === 'hardware'" },
      { "id": "software",  "label": "Software",  "expression": "{{trigger.record.category}} === 'software'" },
      { "id": "network",   "label": "Network",   "expression": "{{trigger.record.category}} === 'network'" }
    ],
    "defaultBranch": "hardware"
  }
}

loop — Iterate Over Array

FieldTypeRequiredDefaultDescription
itemsstring (expr)requiredExpression resolving to the array to iterate over. e.g. "{{trigger.record.assignees}}"
modeenumoptionalsequentialsequential (one at a time) · parallel (all at once, up to concurrency limit).
concurrencyintegeroptional10Max parallel iterations for parallel mode.
itemVariablestringoptionalitemVariable name for the current item in iteration. Access as {{vars.item.*}}.
indexVariablestringoptionalindexVariable name for the current index (0-based).
breakOnstringoptionalExpression evaluated after each iteration. If true, loop stops early.

delay — Pause Execution

FieldTypeRequiredDefaultDescription
modeenumrequiredduration (fixed wait) · until_time (wait until a timestamp) · until_condition (poll until true).
durationstringoptionalFor mode=duration. ISO 8601 duration: "PT5M" (5 min), "PT1H30M" (90 min), "P1D" (1 day).
untilstringoptionalFor mode=until_time. ISO 8601 timestamp or expression resolving to one.
conditionstringoptionalFor mode=until_condition. Re-evaluated every pollIntervalMs until true.
pollIntervalMsintegeroptional30000How often to re-evaluate the condition. Min 5000ms.
maxWaitMsintegeroptional86400000Absolute maximum wait time in ms (default 24h). Throws if exceeded.

transform — Shape Data

FieldTypeRequiredDefaultDescription
expressionstringrequiredA JSONata expression to transform the input data. The entire run context is available as $.
inputobjectoptionalNamed variables to pass to the JSONata expression. Defaults to the full run context.
outputAsstringoptionalVariable name to store the result. Defaults to "output" — access as {{nodes.nodeId.output}}.
json
{
  "id": "reshape-incident",
  "type": "transform",
  "config": {
    "expression": "{ 'title': record.title, 'priority': record.severity & '-' & record.urgency, 'owners': record.cmdbItems.ownerEmail[] }",
    "input": { "record": "{{trigger.record}}" }
  }
}

approval — Human Approval Gate

Pauses the run and waits for a human to approve or reject before continuing.

FieldTypeRequiredDefaultDescription
approversobjectrequired{ type: "user"|"team"|"expression", value: string }. Who can approve.
titlestringrequiredTitle of the approval request shown to approvers.
descriptionstringoptionalDetails shown to approvers. Template expressions supported.
dueInstringoptionalISO 8601 duration. If not approved within this time, onTimeout fires.
onTimeoutenumoptionalrejectreject · approve · skip · fail. What happens if the approval times out.
notifyChannelsstring[]optionalNotification channel IDs to use for approval request messages.
fieldsobject[]optionalAdditional form fields approvers must fill out. Schema same as catalog form fields.

http_request — Outbound HTTP

FieldTypeRequiredDefaultDescription
methodenumrequiredGET · POST · PUT · PATCH · DELETE
urlstringrequiredFull URL. Template expressions supported.
headersobjectoptionalKey-value header map. Values support template expressions.
queryParamsobjectoptionalURL query parameters. Merged with any params in the URL.
bodyanyoptionalRequest body. Serialized as JSON unless content-type header overrides.
authobjectoptional{ type: "bearer"|"basic"|"api_key", ... }. Auth applied to the request.
followRedirectsbooleanoptionaltrueWhether to follow HTTP 3xx redirects.
validateStatusstringoptional2xxStatus range considered success: "2xx" · "any" · specific code like "200".
responseTypeenumoptionaljsonjson · text · binary. How to parse the response body.
connectorIdstringoptionalOptionally route through a configured connector for rate-limit and circuit-breaker tracking.

set_variable — Store Values

FieldTypeRequiredDefaultDescription
variablesobjectrequiredKey-value pairs to set. Values support template expressions. Available in later nodes as {{vars.KEY}}.
scopeenumoptionalrunrun (this execution only) · workspace (persisted across runs — use with care).

subWorkflow — Call Another Workflow

FieldTypeRequiredDefaultDescription
workflowIdstringrequiredID of the workflow to call. Must be active (isActive: true).
inputMappingobjectoptionalMap parent context paths to child payload keys: { childKey: "parent.ctx.path" }.
modeenumoptionalsyncsync (wait for completion, max 30s) · async (fire and forget).
Sub-workflow must not contain a trigger node
The workflow referenced by workflowId must not have a trigger node — the executor calls it directly. Remove the trigger node before activating a sub-workflow. Sub-workflows can be nested up to 5 levels deep.

Outputs of each completed sub-workflow node are returned as nodeId_output keys, e.g. { "n2_output": { ... }, "n3_output": { ... } }. Access them in the parent workflow as ctx.nodeOutputs.subWorkflowNodeId.n2_output.

parallel — Fork Execution

Runs multiple branches simultaneously. Execution resumes at a merge node once all branches complete.

FieldTypeRequiredDefaultDescription
branchesstring[]requiredArray of node IDs that are the start of each parallel branch.
waitForenumoptionalallall (wait for every branch) · any (continue as soon as one branch finishes).

log — Debug Output

FieldTypeRequiredDefaultDescription
levelenumoptionalinfodebug · info · warn · error. Visible in the run trace.
messagestringrequiredLog message. Template expressions supported.
dataanyoptionalAdditional structured data to attach to the log entry.

error_handler — Catch Errors

Catches errors thrown by upstream nodes and routes to a recovery branch. Must be connected via an onError edge.

FieldTypeRequiredDefaultDescription
catchErrorsstring[]optionalError codes to catch. Empty = catch all errors.
errorVariablestringoptionalerrorVariable name for the error object: { code, message, nodeId, attemptCount }.

whatsapp — Send WhatsApp Message

Dispatches a WhatsApp message through the tenant's configured provider (Twilio or Meta Cloud API, set under Settings → Feedback Channels → WhatsApp — see the WhatsApp channel docs). Used by the built-in WhatsApp workflow templates (see the Automations reference).

FieldTypeRequiredDefaultDescription
tostringoptionalRecipient phone number, an expression, or the literal "owner" (also accepts "@owner", "company_owner", "company owner", "admin") to resolve the tenant's configured owner alert number. Either this or toPhoneNumber/phone is required.
toPhoneNumberstringoptionalAlias for to — checked if to is not set.
phonestringoptionalSecond fallback alias for to.
messagestringrequiredMessage body. Template expressions supported. Either this or body/text is required.
mediaUrlstringoptionalMedia URL to attach — appended on its own line to the message text if not already present (no native media API call).
waConfigobjectoptionalOverride the resolved provider credentials for this node instead of reading the tenant's Settings → Feedback Channels → WhatsApp config.
targetstringoptionalSet to "owner" together with an empty to field as an alternative way to route to the owner alert number.
sendRealInTestbooleanoptionalfalseAlso accepted as sendLive / live. Sends a real message even during a Canvas test run instead of simulating it.
continueOnErrorbooleanoptionalfalseIf true, a delivery failure returns { sent: false, ... } instead of throwing and failing the run.

Phone number formatting

The resolved to value is auto-formatted before dispatch: a bare 10-digit Indian mobile number (starts with 6–9) is prefixed +91; any other 11–15 digit number with no leading + gets one prepended. Numbers already in E.164 form (+...) pass through unchanged.

Dry-run / simulated sends

During a Canvas test run the node does not call Twilio/Meta by default — it returns a simulated result ({ sent: true, simulated: true, deliveryStatus: "sent_via_twilio", note: "..." }) so test runs never burn live WhatsApp credits. The same simulation also fires outside test runs whenever to contains example.com, <, or mock. Set sendRealInTest (or sendLive / live) to true on the node config to force a real send during a test run.

If no WhatsApp provider is configured for the tenant, the node always resolves to { sent: false, deliveryStatus: "not_configured" } instead of throwing — regardless of continueOnError. Any other delivery failure (Twilio/Meta API error) throws and fails the run unless continueOnError is true. Check deliveryStatus in downstream nodes if you branch on delivery outcome.