API/Manage Email Workflows API

Manage Email Workflows API

Create, read, replace, publish, and delete email workflows from JSON.

Author workflows outside the UI — from a script, a CI job, or an AI agent. A workflow is a JSON document, so it can live in your repository and be reviewed like code.

Endpoints

MethodPathEffect
POST/api/v1/email/workflowsCreate. Requires name and a non-empty steps.
GET/api/v1/email/workflows/:idRead one workflow with its full step tree.
PUT/api/v1/email/workflows/:idFull replace. Requires a complete document.
PATCH/api/v1/email/workflows/:idPartial update. Any omitted field is left alone.
DELETE/api/v1/email/workflows/:idDelete the workflow, its steps, and its enrollments.

GET /api/v1/email/workflows lists workflows. It returns only API request workflows unless you pass ?trigger=all (or a specific trigger).

The workflow document

Steps nest: a condition step carries its own yes and no arrays. Everything else is a flat list executed in order.

{
  "name": "Onboarding",
  "trigger": "tag_added",
  "status": "draft",
  "triggerTags": ["signup"],
  "triggerTagMode": "all",
  "triggerExcludeTags": ["customer"],
  "steps": [
    {
      "kind": "email",
      "subject": "Welcome",
      "previewText": "Glad you're here",
      "contentBlocks": [
        { "type": "heading", "props": { "text": "Welcome" } },
        { "type": "text", "props": { "markdown": "Thanks for signing up." } }
      ]
    },
    { "kind": "delay", "delayMs": 86400000 },
    {
      "kind": "condition",
      "branch": { "condition": "opened_previous" },
      "yes": [{ "kind": "action", "action": { "type": "add_tag", "tag": "engaged" } }],
      "no": [{ "kind": "email", "subject": "Second try", "contentBlocks": [] }]
    }
  ]
}

Workflow fields

FieldTypeDescription
namestringRequired on create.
triggerstringmanual, contact_created, tag_added, affiliate_customer_created, capture_page_submitted, api_request. Defaults to manual.
statusstringdraft, active, paused. Defaults to draft.
triggerTagsstring[]Tags a contact must carry to enter.
triggerTagModestringall (default) or any.
triggerExcludeTagsstring[]Tags that keep a contact out — checked on entry and before every step.

Step fields

FieldApplies toDescription
kindallemail, delay, condition, action. Defaults to email.
subjectemailRequired. Other kinds get a default label.
contentBlocksemailRequired and non-empty.
previewTextemailInbox preview line.
delayMsallFixed wait before the step runs. Defaults to 0.
waitUntilall{ "hour": 9, "dayOfWeek": 1, "timezone": "Europe/Paris" }. Overrides delayMs. Omit dayOfWeek to wait for the next occurrence of that hour on any day.
branchconditionRequired. See below.
actionactionRequired. { "type": "add_tag" | "remove_tag" | "unsubscribe", "tag": "…" }. tag is required for the two tag types.
yes / noconditionChild steps. Only valid on a condition.

branch.condition is one of opened_previous, clicked_previous, has_tag (needs tag), or day_of_week (needs dayOfWeek 0–6, optional dayComparison of before/on/after).

A workflow is capped at 100 steps.

Create

curl -X POST https://growth.example.com/api/v1/email/workflows \
  -H "x-api-key: org_api_key" \
  -H "Content-Type: application/json" \
  -d @onboarding.json
{
  "workflow": { "id": "n97…", "name": "Onboarding", "status": "draft", "steps": [] },
  "activeEnrollments": 0,
  "enrollmentsSampleLimited": false
}

Validation errors point at the exact path:

{ "message": "steps[3].no[0].action.tag is required for \"add_tag\"" }

Publish or pause

PATCH only the field you want, without resending the tree:

curl -X PATCH https://growth.example.com/api/v1/email/workflows/n97… \
  -H "x-api-key: org_api_key" \
  -H "Content-Type: application/json" \
  -d '{"status":"active"}'

Delete

curl -X DELETE "https://growth.example.com/api/v1/email/workflows/n97…?force=true" \
  -H "x-api-key: org_api_key"

Without force, a workflow with active enrollments returns 409 rather than dropping contacts mid-flow. Send history in Emails is preserved either way.

Error responses

StatusMeaning
400Invalid JSON body, or a document that failed validation.
401Missing or invalid API key.
404Unknown workflow, or one belonging to another organization.
409Delete refused: contacts are still enrolled. Retry with ?force=true.

Notes

  • Steps are renumbered on every save, in tree order. If a condition uses the legacy onTrueGoToOrder / onFalseGoToOrder jump targets (which point at absolute step orders), re-check them after a round-trip.
  • Replacing the tree affects contacts mid-flow. They track their position by step order, so they resume at whatever now sits at that position. The response reports activeEnrollments so you can see when this applies.
  • The API is stricter than the UI. The editor lets you drop an add_tag node and fill the tag in later; the API rejects it, because a workflow that cannot run should not be created from a script.
  • growth-cli wraps all of this: workflows create --file, workflows get --export for a round-trip edit, workflows publish. See the growth-cli guide.
List Email Workflows APIManage Transactional Emails API