Workflows
Build visual automation workflows using a drag-and-drop DAG (directed acyclic graph) editor. Workflows connect trigger nodes to action nodes, enabling complex email operations without code.
Create a workflow
/v1/workflowsCreate a new workflow in draft status.
Request body
| Parameter | Type | Description |
|---|---|---|
namerequired | string | Workflow name. |
description | string | Optional description of what the workflow does. |
{
"id": "wf_abc123",
"name": "Bounce Handler",
"description": "Suppress permanently bounced emails and notify Slack",
"status": "draft",
"nodes": [],
"connections": [],
"createdAt": "2026-03-21T00:00:00Z"
}List workflows
/v1/workflowsList all workflows for the current team.
Get a workflow
/v1/workflows/{id}Retrieve a workflow with its full node and connection graph.
Update a workflow
/v1/workflows/{id}Update workflow name, description, nodes, or connections.
Delete a workflow
/v1/workflows/{id}Delete a workflow. Active workflows must be deactivated first.
Activate or deactivate
/v1/workflows/{id}/activateToggle a workflow between active and inactive. Active workflows respond to their trigger events.
Active workflows are read-only — editing (PATCH) is rejected while a workflow is active. Deactivate it first; draft and inactive workflows are editable.
Duplicate a workflow
/v1/workflows/{id}/duplicateClone a workflow's node/connection graph into a new draft. Execution history is not copied, and the copy gets its own webhook trigger key/URL.
Manually trigger execution
/v1/workflows/{id}/executionsManually trigger a workflow execution. Optionally pass trigger data in the request body.
Send an optional Idempotency-Key header to make the trigger safe to retry: a repeat request with the same key on the same workflow returns the original execution instead of firing a second run. The same header is honoured on the public webhook trigger (POST /v1/workflows/triggers/{publicKey}).
List execution history
/v1/workflows/{id}/executionsList executions for a workflow with cursor-based pagination, most recent first.
Query parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by execution status: running, completed, failed, cancelled, or waiting. |
limit | number | Results per page (default: 20, max: 100). |
cursor | string | Cursor for pagination (the cursor value from the previous response). |
Example response
Each list item carries a truncated error preview (~200 chars) so failures can be spotted without opening every run; the full error and per-node detail live on the detail endpoint below.
{
"data": [
{
"id": "8f1c0c2e-9a3b-4f7d-8e21-7b2d9c4a1e55",
"workflowId": "2b9a7d10-4c3e-4a1f-9d52-1e6f3b8c0a44",
"status": "failed",
"nodeCount": 3,
"startedAt": "2026-06-02T10:30:00Z",
"completedAt": "2026-06-02T10:30:02Z",
"error": "send_email failed: smtp 550 rejected"
}
],
"cursor": "2026-06-02T10:30:00.0000000Z",
"hasMore": true
}Get execution detail
/v1/workflows/{id}/executions/{exec_id}Get detailed execution results including the trigger payload and per-node status, input/output data, and errors.
Example response
{
"id": "8f1c0c2e-9a3b-4f7d-8e21-7b2d9c4a1e55",
"workflowId": "2b9a7d10-4c3e-4a1f-9d52-1e6f3b8c0a44",
"status": "failed",
"triggerData": { "email_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" },
"error": "send_email failed: smtp 550 rejected",
"nodes": [
{
"id": "c4a1e556-7b2d-9c4a-8e21-9a3b4f7d8f1c",
"nodeId": "b1f2...",
"nodeType": "send_email",
"status": "failed",
"inputData": { "to": "user@example.com" },
"outputData": null,
"error": "smtp 550 rejected",
"startedAt": "2026-06-02T10:30:01Z",
"completedAt": "2026-06-02T10:30:02Z"
}
],
"startedAt": "2026-06-02T10:30:00Z",
"completedAt": "2026-06-02T10:30:02Z"
}Rerun an execution
/v1/workflows/{id}/executions/{exec_id}/rerunCreate a new execution from a finished one. The original run is left unchanged as an audit record.
Request body
| Parameter | Type | Description |
|---|---|---|
fromFailedNode | boolean | Default false replays the whole graph. true preserves the original run's completed-prefix node outputs and re-runs from the failed node onward (only valid when the source execution failed). |
Returns 201 with the new execution (same shape as the detail endpoint). Only finished executions can be rerun — a run that is still running or waiting returns 400.
Execution status
| Parameter | Type | Description |
|---|---|---|
running | status | Execution is in progress. |
completed | status | All nodes finished successfully. |
failed | status | A node failed after exhausting its retries; see the error field. |
cancelled | status | Execution was cancelled before completing. |
waiting | status | Parked on a long delay node; a scheduler resumes it when the delay elapses. |
Node status
| Parameter | Type | Description |
|---|---|---|
pending | status | Node has not started yet. |
running | status | Node is executing. |
completed | status | Node finished successfully. |
failed | status | Node failed after exhausting its retries. |
skipped | status | Node was skipped — the un-taken branch of a condition node. |
waiting | status | Delay node sleeping on a long timer; flips to completed when it fires. |
Workflow status
| Parameter | Type | Description |
|---|---|---|
draft | status | Workflow is being designed. Does not respond to triggers. |
active | status | Workflow is live and will execute when triggered. |
inactive | status | Workflow is paused. Will not respond to triggers until reactivated. |
Trigger nodes
Every workflow starts with a trigger node that defines when the workflow runs:
| Parameter | Type | Description |
|---|---|---|
email_event | trigger | Fires on email events: delivered, bounced, opened, clicked, complained. |
webhook_received | trigger | Fires when an external HTTP POST hits the workflow unique URL. |
schedule | trigger | Fires on a recurring cron schedule (e.g. daily digest, weekly report). |
manual | trigger | Triggered manually from the dashboard or via the API. |
contact_event | trigger | Fires when a contact is created, updated, unsubscribed, or added to a segment. |
broadcast_complete | trigger | Fires when a broadcast finishes sending. |
Action nodes
Action nodes define the steps in your workflow:
| Parameter | Type | Description |
|---|---|---|
send_email | action | Send a transactional email via the Txtly API. |
send_broadcast | action | Trigger a broadcast to a segment. |
http_request | action | Make an external HTTP call (any method, headers, body). |
delay | action | Wait for a specified duration before continuing. |
condition | action | Branch based on a condition. Outputs: true and false paths. |
filter | action | Continue only if a condition is met, otherwise stop this branch. |
add_segment | action | Add a contact to a segment. |
remove_segment | action | Remove a contact from a segment. |
update_contact | action | Modify contact properties. |
suppress_email | action | Add an email address to the suppression list. |
slack_notification | action | Send a message to a Slack channel via webhook. |
discord_notification | action | Send a message to a Discord webhook. |
transform_data | action | Map and transform data between nodes using JSONPath or templates. |
code | action | Run custom C# code for advanced logic. |
HTTP Request node
The http_request action calls any external HTTP API as part of a workflow. Non-2xx responses do not fail the node — the runner returns the full response so a downstream condition node can branch on node_{id}.status_code. The orchestrator only retries on network failure or unresolvable credential.
Config
| Parameter | Type | Description |
|---|---|---|
urlrequired | string | Absolute http:// or https:// URL. file://, ftp:// etc. are rejected. |
methodrequired | string | One of GET, POST, PUT, PATCH, DELETE. |
headers | object | Free-form string→string map of request headers. Applied before auth so a credential-backed Authorization wins on conflict. |
body | string | Request body. Ignored for GET and DELETE. Set Content-Type via headers. |
auth_type | string | One of none (default), bearer, basic, header. |
auth_header_name | string | Required when auth_type=header — the header to inject the credential's value under. |
credential_id | string | UUID of a Credential the team owns. Required for any auth_type other than none. |
Output
The runner sets node_{id} in the execution context to:
{
"status_code": 200,
"ok": true,
"headers": { "Content-Type": "application/json", "X-Trace-Id": "..." },
"body": "{\"result\":\"ok\"}"
}Execution engine
Workflows are executed as a DAG using topological sort to determine execution order. Independent branches run in parallel, while nodes within a branch run sequentially. Each node's output is available to downstream nodes as input data. Execution state is persisted at each step for observability and resume capability.
Example: Bounce Handler
[Email Bounced] --> [Condition: bounce_type == 'permanent'?]
--> true: [Suppress Email] --> [Slack: "Permanent bounce: {{email}}"]
--> false: [Delay: 1 hour] --> [Send Email: retry original]