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".
| Field | Type | Notes |
|---|---|---|
id | number | Use as workflowId. |
name | string | |
description | string? | |
status | enum | ACTIVE · DRAFT · PAUSED · ARCHIVED. |
triggerType | enum? | 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). |
runsCount | number | Total executions. |
runsFailedCount | number | Total failed executions. |
lastRunDate | string? | ISO timestamp of the most recent run. |
liveSnapshotId | string? | Published snapshot. |
draftSnapshotId | string? | Working draft. |
| Type | Purpose |
|---|---|
TRIGGER | Event-based entry point (conversation ended, contact created, …). |
SCHEDULED_TRIGGER | Cron-style entry point. |
WEBHOOK | Entry point fed by an Incoming Webhook. |
API | Call an external HTTP endpoint. |
TOOLS_AI | Let the AI choose a Tool to call. |
CONDITIONAL_ROUTING | Branch based on AI-evaluated conditions. |
AI_CAPTURE | Extract structured data from text using an LLM. |
DATA_TRANSFORMER | Reshape data via prompt. |
DYNAMIC_TABLES | Create, update, delete, search, or change the record type of records in a Table or Object. |
CREATE_RECORD_ACTIVITY | Log a manual activity (note, call, meeting, email, WhatsApp) on a specific Object record. |
ITERATION | Loop over an array variable (body / completed / empty handles). |
BREAK | Exit the current loop early and continue on the loop's completed path. |
AUTOMATION_STATUS | Set another automation live, draft, or toggle its status. |
SEND_MESSAGE | Push a message to a conversation. |
SEND_WHATSAPP_MESSAGE | Send a WhatsApp Business template to a phone number or People record. |
TRANSCRIPTION | Transcribe an audio URL. |
FILE_ANALYSIS | OCR + AI analysis of a file from a URL. |
Each node carries nodeId, alias, position, type, and a typed data payload.
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.
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:
| Field | Value |
|---|---|
connectedAccountId | Numeric id of the connected account, from GET /public/v1/integrations. Must belong to your account. |
triggerToolkit | The toolkit that owns the event, uppercase (GOOGLEDRIVE, GMAIL, OUTLOOK, HUBSPOT, …). |
triggerSlug | The 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.
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
data—aiModelId,customToolIds,tableId,recordTypeId,triggeredByAgentIds[],triggerByWebhookIds[],connectedAccountId,knowledgeBaseIds, etc. If any referenced id does not exist or does not belong to your account, the request returns400 bad_requestwithdetails.issues[].code === "not_found"and the node is not persisted. See Errors.Schema-level checks also apply to
SCHEDULED_TRIGGER(cronExpressionvalidated bycron-validate,timezoneagainst IANA),WEBHOOKandAPI(urlmust be a valid URL).
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.
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}"
}
}| Field | Required | Notes |
|---|---|---|
recordTypeId | ✅ | Numeric id of the Object's record type. Validated: must exist and belong to your account. |
rowId | ✅ | MongoDB ObjectId of the record to log on. Supports {varName} interpolation. |
activityType | ✅ | One of: NOTE, EMAIL, PHONE_CALL, MEETING, WHATSAPP. Validated at save time. |
content | ✅ | Activity 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.
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
}
}| Field | Notes |
|---|---|
variableName | Name or alias of an existing workflow variable. Must resolve to an array at runtime. |
variablePath | Optional dot path inside the variable (e.g. data.items when the variable holds a full API response). |
continueOnError | When 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.
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" }
}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.
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}" } }
}
}| Field | Notes |
|---|---|
recipientMode | PHONE_NUMBER (default) or PEOPLE_RECORD. |
personName | Required in PHONE_NUMBER mode. Supports {var} interpolation. |
phoneNumber | Required in PHONE_NUMBER mode. Full international format. Supports {var}. |
peopleRowId | Required in PEOPLE_RECORD mode. People record ObjectId. Use {var} or {<loopNodeId>.item} when looping scalars — not {<loopNodeId>.item.id}. |
phoneNumberId | Meta WhatsApp Business phone number id. Must exist and have an assistantId (validated). |
template | Approved template name. |
templateVariables | Maps template parameter ids to values (body, header, buttons). |
Resolve phoneNumberId and template via the read-only channels endpoints below.
| Verb | Path | Purpose |
|---|---|---|
GET | /public/v1/channels | List connected WhatsApp numbers, Instagram accounts, Messenger pages |
GET | /public/v1/channels/whatsapp/templates | List APPROVED WhatsApp templates from Meta for this account |
For SEND_WHATSAPP_MESSAGE, use a WhatsApp number where canSendMessages is true (an assistant is assigned).
| Verb | Path | Purpose |
|---|---|---|
GET | /public/v1/integrations | List 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.
| Verb | Path | Purpose |
|---|---|---|
GET | /public/v1/workflows | List, with status filter |
POST | /public/v1/workflows | Create |
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}/graph | Full graph |
POST | /public/v1/workflows/{workflowId}/nodes | Create 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}/edges | Add edge |
DELETE | /public/v1/workflows/{workflowId}/edges | Remove edge |
GET | /public/v1/workflows/{workflowId}/analytics | Run analytics |
GET | /public/v1/workflows/{workflowId}/logs | List run logs (history) |
GET | /public/v1/workflows/{workflowId}/logs/{logId} | One run + per-node results |
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):
| Field | Notes |
|---|---|
id | Use as logId. |
successful | Whether the run completed without error. |
started_at | ISO timestamp. |
completed_at | ISO timestamp (null while running). |
duration | Milliseconds. |
operations | Number of node operations executed. |
ai_credits | AI credits consumed. |
error | Error message if the run failed. |
insufficient_credits | Run stopped because the account ran out of credits. |
prevented_loop | Run 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.
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