Skip to content
Last updated

A Workflow is an automation graph that runs at the account level — not tied to any single agent. Workflows fire on triggers (conversation events, schedules, incoming webhooks), execute a graph of nodes, and can call external APIs, transform data, write to Tables, send messages, etc.

If a Flow is "what the agent says next", a Workflow is "what happens around the agent".

Identity

FieldTypeNotes
idnumberUse as workflowId.
namestring
descriptionstring?
statusenumACTIVE · DRAFT · PAUSED · ARCHIVED.
triggerTypeenum?Workflow-level metadata: CONVERSATION_ENDED · CONVERSATION_IDLE · FEEDBACK_CAPTURED · CONTACT_CREATED · CONTACT_UPDATED · TABLE_ROW_CREATED · TABLE_ROW_UPDATED · OBJECT_RECORD_CREATED · OBJECT_RECORD_UPDATED · INCOMING_WEBHOOK · SCHEDULED_TRIGGER · COMPOSIO_TRIGGER. SCHEDULED_TRIGGER here means the automation uses a Scheduled Trigger node — it is not a valid data.triggerType on a TRIGGER node (see below).
runsCountnumberTotal executions.
runsFailedCountnumberTotal failed executions.
lastRunDatestring?ISO timestamp of the most recent run.
liveSnapshotIdstring?Published snapshot.
draftSnapshotIdstring?Working draft.

The graph

Node types

TypePurpose
TRIGGEREvent-based entry point (conversation ended, contact created, …).
SCHEDULED_TRIGGERCron-style entry point.
WEBHOOKEntry point fed by an Incoming Webhook.
APICall an external HTTP endpoint.
TOOLS_AILet the AI choose a Tool to call.
CONDITIONAL_ROUTINGBranch based on AI-evaluated conditions.
AI_CAPTUREExtract structured data from text using an LLM.
DATA_TRANSFORMERReshape data via prompt.
DYNAMIC_TABLESCreate, update, delete, search, or change the record type of records in a Table or Object.
CREATE_RECORD_ACTIVITYLog a manual activity (note, call, meeting, email, WhatsApp) on a specific Object record.
ITERATIONLoop over an array variable (body / completed / empty handles).
BREAKExit the current loop early and continue on the loop's completed path.
AUTOMATION_STATUSSet another automation live, draft, or toggle its status.
SEND_MESSAGEPush a message to a conversation.
SEND_WHATSAPP_MESSAGESend a WhatsApp Business template to a phone number or People record.
TRANSCRIPTIONTranscribe an audio URL.
FILE_ANALYSISOCR + AI analysis of a file from a URL.

Each node carries nodeId, alias, position, type, and a typed data payload.

Conversation triggers are bound to agents

CONVERSATION_ENDED, CONVERSATION_IDLE, and FEEDBACK_CAPTURED fire per agent: the workflow runs only for the agents whose ids are listed in data.triggeredByAgentIds (agent UUIDs from Agents). A trigger created without that array is accepted and then never fires. Related pitfalls: setting data.triggeredBy to USER blocks these triggers, since conversation events always originate from the agent, and CONVERSATION_IDLE additionally depends on enableIdle plus idleSettings being configured on the agent's channel settings.

Record triggers need their target as well: TABLE_ROW_CREATED / TABLE_ROW_UPDATED require data.triggeredByTableId, and OBJECT_RECORD_CREATED / OBJECT_RECORD_UPDATED require data.triggeredByTableId plus data.triggeredByRecordTypeId.

Integration triggers (COMPOSIO_TRIGGER)

A TRIGGER node with triggerType: COMPOSIO_TRIGGER starts the workflow from a third-party event — a file created in Google Drive, a new Gmail or Outlook message, a HubSpot contact, a Stripe checkout. It requires three fields in data:

FieldValue
connectedAccountIdNumeric id of the connected account, from GET /public/v1/integrations. Must belong to your account.
triggerToolkitThe toolkit that owns the event, uppercase (GOOGLEDRIVE, GMAIL, OUTLOOK, HUBSPOT, …).
triggerSlugThe provider event, e.g. GOOGLEDRIVE_FILE_CREATED_TRIGGER. An unsupported slug returns 400 with Unsupported trigger: <slug>.

Discovering slugs and their config. GET /public/v1/integrations/trigger-types (optionally ?toolkit=GOOGLEDRIVE) returns every supported event with its slug, toolkit, and configFields. Each config field reports whether it is required, whether it is userConfigurable, and a resourceType when its value has to be looked up. GET /public/v1/integrations/trigger-resources?resourceType=…&connectedAccountId=… resolves those values — Drive folders and shared drives, Sheets spreadsheets and tabs, Asana workspaces and projects, Salesforce sobjects and fields, Gmail labels, Outlook folders and calendars, OneDrive folders. Pass parent when browsing inside another resource (spreadsheetId, workspaceGid, sobjectName).

triggerConfig narrows the subscription and is validated per slug: an undeclared key returns Unknown trigger config field for <slug>: …, an invalid value returns Invalid trigger config: <field> - <message>, and platform-managed fields (userConfigurable: false, typically interval) are ignored if sent. Google Sheets, Asana, YouTube, and HubSpot events cannot be created without their required fields. Values are not checked against the provider, so a well-formed but wrong id subscribes successfully and then never fires — resolve ids through trigger-resources rather than by hand.

Saving the node subscribes to the event with the provider and writes the resulting connectedAccountTriggerId back into the node — it is an output field, so do not send it yourself. Updating the node re-subscribes with the new configuration and deleting the node removes the subscription, so a change of event or account means editing the existing trigger rather than adding a second one. Delivered events are queued only for automations that are ACTIVE; the payload is exposed to downstream nodes as the built-in {trigger_payload} variable.

Slack has Tools but no trigger, so a workflow cannot be started from a Slack message.

Scheduled triggers

Cron entry points are the separate SCHEDULED_TRIGGER node type, not a triggerType on a TRIGGER node. Do not send "triggerType":"SCHEDULED_TRIGGER" inside a TRIGGER node's data — the API rejects it. The workflow's top-level triggerType field may still be SCHEDULED_TRIGGER after you add a scheduled trigger node. data requires cronExpression and timezone, plus startTime and endTime, which are nullable but not optional — send null when the schedule has no daily window. startDate, endDate, and frequency (INTERVALS · DAILY · WEEKLY · MONTHLY) are optional, and frequency is only a label: the cron expression is what schedules the run.

The underlying job is registered while the automation is ACTIVE, whether the node is created before or after activation. Moving the workflow back to DRAFT disables it.

Reference validation: on create/update, the API validates every cross-resource reference inside dataaiModelId, customToolIds, tableId, recordTypeId, triggeredByAgentIds[], triggerByWebhookIds[], connectedAccountId, knowledgeBaseIds, etc. If any referenced id does not exist or does not belong to your account, the request returns 400 bad_request with details.issues[].code === "not_found" and the node is not persisted. See Errors.

Schema-level checks also apply to SCHEDULED_TRIGGER (cronExpression validated by cron-validate, timezone against IANA), WEBHOOK and API (url must be a valid URL).

Runtime variables

Automation workflows expose one runtime variable per node, with name = nodeId. Since node ids follow node_<uuid>, a node saved as node_2222… is referenced downstream as {node_2222…} — read the exact id from the graph. This is in addition to explicitly declared Workflow Variables.

Interpolable fields include the same set as Flows, plus:

  • AI Capture: prompt, instructions.
  • Data Transformer: prompt.
  • Tools AI: instructions, prompt.
  • Transcription: audioUrl.
  • Create Record Activity: rowId, content.
  • WhatsApp template variables: templateVariables.header, body, buttons.

Capturing specific values. The API node can extract values from its JSON response into named variables via its variables field ({ key, value, fullResponse }), and AI_CAPTURE / TOOLS_AI / TRANSCRIPTION populate variables via captureVariables. All capture targets must reference a variable that already exists (create it first via POST /workflows/{workflowId}/variables). In captureVariables you may reference it by { "name": "<var>" } or { "id": <id> } — the API resolves and links it to the canonical { id, name, description }; an unknown name/key returns 400. For the API node's value path syntax (dot/[n] property access into the JSON response), see Flows → API node response paths.

CREATE_RECORD_ACTIVITY node

Logs a manual activity on a specific Object record at workflow runtime.

{
    "nodeId": "node_44444444-4444-4444-4444-444444444444",
    "type": "CREATE_RECORD_ACTIVITY",
    "position": { "positionX": 640, "positionY": 0 },
    "data": {
        "type": "CREATE_RECORD_ACTIVITY",
        "recordTypeId": 5,
        "rowId": "{contact_row_id}",
        "activityType": "PHONE_CALL",
        "content": "Llamada de seguimiento: {call_summary}"
    }
}
FieldRequiredNotes
recordTypeIdNumeric id of the Object's record type. Validated: must exist and belong to your account.
rowIdMongoDB ObjectId of the record to log on. Supports {varName} interpolation.
activityTypeOne of: NOTE, EMAIL, PHONE_CALL, MEETING, WHATSAPP. Validated at save time.
contentActivity body text. Supports {varName} interpolation.

Validation: recordTypeId must point to a real record type on an Object (not a Table) in your account. activityType must be a valid enum value. Variables in rowId and content must exist — the API returns 400 with the offending variable name if any reference is unknown.

ITERATION (Loop) node

Iterates over an array stored in a workflow variable. The wire type is ITERATION.

{
    "nodeId": "node_66666666-6666-6666-6666-666666666666",
    "type": "ITERATION",
    "alias": "Loop",
    "position": { "positionX": 320, "positionY": 0 },
    "data": {
        "type": "ITERATION",
        "variableName": "items",
        "variablePath": "data.items",
        "continueOnError": false
    }
}
FieldNotes
variableNameName or alias of an existing workflow variable. Must resolve to an array at runtime.
variablePathOptional dot path inside the variable (e.g. data.items when the variable holds a full API response).
continueOnErrorWhen true, a failed item inside the loop is skipped. When false, any failure stops the automation.

Limits: At most 50 items per loop invocation. If the resolved array contains more than 50 items, the automation run fails before the loop body starts; no items are processed. The whole automation is also capped at 500 node execution steps (each executed node counts one step), which can stop a loop after some items.

Edges: connect three outgoing handles — body (for-each), completed (after all items), empty (null/empty list; does not route to completed). Inside the loop, {<loopNodeId>.item}, {<loopNodeId>.index}, and {<loopNodeId>.length} are available (use the loop node's nodeId from the graph). {<loopNodeId>.item} is the entire current item (JSON if object) — there is no {<loopNodeId>.item.field} dot-access. DYNAMIC_TABLES SEARCH stores a top-level array of rows; leave variablePath empty when looping that output.

BREAK node

Exits the current loop and continues on the loop's completed path. Place only inside a Loop body branch; outside an active loop the automation fails.

{
    "nodeId": "node_77777777-7777-7777-7777-777777777777",
    "type": "BREAK",
    "position": { "positionX": 640, "positionY": 0 },
    "data": { "type": "BREAK" }
}

AUTOMATION_STATUS node

Changes another automation's status (SET_ACTIVE, SET_DRAFT, or TOGGLE). Does not publish draft graph changes.

{
    "nodeId": "node_88888888-8888-8888-8888-888888888888",
    "type": "AUTOMATION_STATUS",
    "position": { "positionX": 320, "positionY": 0 },
    "data": {
        "type": "AUTOMATION_STATUS",
        "automationId": 1,
        "action": "SET_ACTIVE"
    }
}

automationId is validated at save time — it must exist in your account.

SEND_WHATSAPP_MESSAGE node

Sends a WhatsApp Business template message. Distinct from SEND_MESSAGE (conversation push).

{
    "nodeId": "node_99999999-9999-9999-9999-999999999999",
    "type": "SEND_WHATSAPP_MESSAGE",
    "position": { "positionX": 320, "positionY": 0 },
    "data": {
        "type": "SEND_WHATSAPP_MESSAGE",
        "recipientMode": "PHONE_NUMBER",
        "personName": "{first_name} {last_name}",
        "phoneNumber": "{phone_number}",
        "phoneNumberId": "1132681309928224",
        "template": "hello_world",
        "templateVariables": { "body": { "1": "{first_name}" } }
    }
}
FieldNotes
recipientModePHONE_NUMBER (default) or PEOPLE_RECORD.
personNameRequired in PHONE_NUMBER mode. Supports {var} interpolation.
phoneNumberRequired in PHONE_NUMBER mode. Full international format. Supports {var}.
peopleRowIdRequired in PEOPLE_RECORD mode. People record ObjectId. Use {var} or {<loopNodeId>.item} when looping scalars — not {<loopNodeId>.item.id}.
phoneNumberIdMeta WhatsApp Business phone number id. Must exist and have an assistantId (validated).
templateApproved template name.
templateVariablesMaps template parameter ids to values (body, header, buttons).

Resolve phoneNumberId and template via the read-only channels endpoints below.

Channels (read-only)

VerbPathPurpose
GET/public/v1/channelsList connected WhatsApp numbers, Instagram accounts, Messenger pages
GET/public/v1/channels/whatsapp/templatesList APPROVED WhatsApp templates from Meta for this account

For SEND_WHATSAPP_MESSAGE, use a WhatsApp number where canSendMessages is true (an assistant is assigned).

Integrations (read-only connected accounts)

VerbPathPurpose
GET/public/v1/integrationsList OAuth/API connected accounts (Google Sheets, Gmail, Slack, etc.)

Use the returned id as connectedAccountId in agent settings, flows, and workflows. Requires a USER API key.

Operations

VerbPathPurpose
GET/public/v1/workflowsList, with status filter
POST/public/v1/workflowsCreate
GET/public/v1/workflows/{workflowId}Detail (?includeNodes=true for nodes)
PUT/public/v1/workflows/{workflowId}Update metadata / status
DELETE/public/v1/workflows/{workflowId}Soft delete + cleanup of triggers
GET/public/v1/workflows/{workflowId}/graphFull graph
POST/public/v1/workflows/{workflowId}/nodesCreate node
PUT/public/v1/workflows/{workflowId}/nodes/{nodeId}Update node
DELETE/public/v1/workflows/{workflowId}/nodes/{nodeId}Delete node + incident edges
POST/public/v1/workflows/{workflowId}/edgesAdd edge
DELETE/public/v1/workflows/{workflowId}/edgesRemove edge
GET/public/v1/workflows/{workflowId}/analyticsRun analytics
GET/public/v1/workflows/{workflowId}/logsList run logs (history)
GET/public/v1/workflows/{workflowId}/logs/{logId}One run + per-node results

Run logs

Every time a workflow runs it records an execution. Read them to audit results, debug failures, or track credit usage.

List /public/v1/workflows/{workflowId}/logs — paginated, with optional status, start_date, and end_date filters. Each run (basic fields):

FieldNotes
idUse as logId.
successfulWhether the run completed without error.
started_atISO timestamp.
completed_atISO timestamp (null while running).
durationMilliseconds.
operationsNumber of node operations executed.
ai_creditsAI credits consumed.
errorError message if the run failed.
insufficient_creditsRun stopped because the account ran out of credits.
prevented_loopRun stopped because a loop was detected.

Detail /public/v1/workflows/{workflowId}/logs/{logId} — the run above plus node_results: [{ node_id, alias, type, success, error, ai_credits, created_at }], one entry per node that executed.

CLI

frontline workflows list --table
frontline workflows create --name "Daily CRM Sync"
frontline integrations trigger-types --toolkit GOOGLEDRIVE --table
frontline integrations trigger-resources --type googledrive_folders --connected-account-id 42 --table
frontline workflows nodes create --data '{"type":"TRIGGER","position":{"positionX":0,"positionY":0},"data":{"type":"TRIGGER","triggerType":"CONTACT_CREATED"}}'
frontline workflows analytics --start-date 2026-01-01 --end-date 2026-12-31
frontline workflows logs --workflow-id 2 --table
frontline workflows logs --workflow-id 2 --status FAILED --start-date 2026-01-01
frontline workflows logs get 9001 --workflow-id 2 --pretty
frontline channels list --table
frontline channels whatsapp-templates --table