Topolo Docs Topolo Platform
Open workspace

Set draft sequence steps

Replace every step of an exact draft sequence version.

Outcome

Replace every step of an exact draft sequence version.

Before you start

  • Verify the environment, credential, authorized organization and the published action permission workspace:write.
  • Discover an accessible Engage workspace and bind resourceType=workspace and its resourceId separately from action input. Fixture identifiers grant no access.
  • Approve the exact mutation and its effects before CLI --confirm or MCP confirm:true. This documentation does not authorize outreach.

Discover the target

  • Discover the sequence through sequences.list using the retained campaignId, then read sequences.get in the same authorized workspace. Inspect sequence.status, draft, published and versions before planning.
  • Select draft.id and read sequences.versions.get. Retain the complete prior ordered steps; the input replaces the entire step set rather than appending.
topolo whoami --json
topolo actions get app_topolo_engage.sequences.versions.steps.set --json
topolo workspaces list topolo-engage --json

Input fields

FieldRequiredMeaning and constraints
idYesRequired sequence identifier discovered through sequences.list in the selected workspace, trimmed non-empty string up to 128 characters.
versionIdYesRequired version identifier from sequences.get versions[], draft or published, trimmed non-empty string up to 128 characters. It must belong to the supplied sequence; version number is not versionId.
stepsYesRequired array of 1–10 steps in execution order. Every step needs delayDays integer 0–90, relative to enrollment for the first step and previous step thereafter. Email (type omitted or email): subjectTemplate trimmed ≤200, bodyTemplate trimmed 1–10,000 and required threadWithPrevious boolean. Tasks (linkedin_task/call_task): instructions trimmed 1–2,000 and optional dueDays integer 0–30. WhatsApp: templateName lowercase letters/digits/underscore 1–512, language such as en_US, and up to 10 ordered allowed merge-field variables. Use the strict current nested schema; do not mix variant fields.

Validate, plan and execute

Set ENGAGE_WORKSPACE_ID to the identifier of your selected workspace. Prepare payload.json from the current input schema and discovered identifiers. Use the same payload for validation, planning and execution.

: "${ENGAGE_WORKSPACE_ID:?Set ENGAGE_WORKSPACE_ID to your selected workspace identifier}"
topolo actions validate app_topolo_engage.sequences.versions.steps.set --data @payload.json --json
topolo actions plan app_topolo_engage.sequences.versions.steps.set --resource-type workspace --resource-id "$ENGAGE_WORKSPACE_ID" --data @payload.json --confirm --json

Review the plan and obtain approval for the exact effects before executing the unchanged request.

: "${ENGAGE_WORKSPACE_ID:?Set ENGAGE_WORKSPACE_ID to your selected workspace identifier}"
topolo actions call app_topolo_engage.sequences.versions.steps.set --resource-type workspace --resource-id "$ENGAGE_WORKSPACE_ID" --data @payload.json --confirm --json

For MCP, use topolo_get_action, topolo_validate_action, topolo_plan_action and topolo_call_action. Pass the selected resource context separately; confirm:true is required for mutations.

Worked requests and responses

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 4,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Quick question, {{first_name}}",
        "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 4,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Following up, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: the run ledger > records a scheduled pass: the heartbeat and one row per workspace touched. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 0,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "first_name",
        "sender_name"
      ]
    },
    {
      "delayDays": 2,
      "subjectTemplate": "Hello {{first_name}}",
      "bodyTemplate": "Hi {{first_name}}",
      "threadWithPrevious": false
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "whatsapp_template",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0,
        "whatsappTemplate": {
          "templateName": "webinar_invite",
          "language": "en_GB",
          "variables": [
            "first_name",
            "sender_name"
          ]
        }
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 2,
        "type": "email",
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: the runner on a WhatsApp template step > sends the approved template to an opted-in contact on channel whatsapp and moves on. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "type": "linkedin_task",
      "delayDays": 0,
      "instructions": "Send a connection request mentioning the webinar.",
      "dueDays": 2
    },
    {
      "delayDays": 3,
      "subjectTemplate": "Hello {{first_name}}",
      "bodyTemplate": "Hi {{first_name}}",
      "threadWithPrevious": false
    },
    {
      "type": "call_task",
      "delayDays": 2,
      "instructions": "Call the main office."
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "linkedin_task",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "Send a connection request mentioning the webinar.",
        "dueDays": 2
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 3,
        "type": "email",
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_3",
        "position": 3,
        "delayDays": 2,
        "type": "call_task",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "Call the main office.",
        "dueDays": 0
      }
    ]
  }
}

Source test: task steps in sequences > stores linkedin_task and call_task steps with instructions and a due offset, and copies them into a new version. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_2",
  "versionId": "eng_seqv_2",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Hello {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_2",
    "sequenceId": "eng_seq_2",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_3",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: engagement analytics > reports first-touch rates by campaign: each contact once, bounced never replied, test campaigns excluded. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_3",
  "versionId": "eng_seqv_3",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 4,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_3",
    "sequenceId": "eng_seq_3",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_4",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Quick question, {{first_name}}",
        "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_5",
        "position": 2,
        "delayDays": 4,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Following up, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: engagement analytics > reports first-touch rates by campaign: each contact once, bounced never replied, test campaigns excluded. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_2",
  "versionId": "eng_seqv_2",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 4,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_2",
    "sequenceId": "eng_seq_2",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_3",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Quick question, {{first_name}}",
        "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_4",
        "position": 2,
        "delayDays": 4,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Following up, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: the run ledger > records a scheduled pass: the heartbeat and one row per workspace touched. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 3,
      "subjectTemplate": "Hello {{first_name}}",
      "bodyTemplate": "Hi {{first_name}}",
      "threadWithPrevious": false
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 3,
        "type": "email",
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: task steps in sequences > refuses an email that threads with no earlier email, and task steps without instructions. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 1,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    },
    {
      "delayDays": 1,
      "subjectTemplate": "",
      "bodyTemplate": "Last one, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Quick question, {{first_name}}",
        "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 1,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Following up, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_3",
        "position": 3,
        "delayDays": 1,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Last one, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: runner with the Nexus transport contract > threads every follow-up from the stored RFC Message-IDs: In-Reply-To the previous, References root … previous. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_3",
  "versionId": "eng_seqv_3",
  "steps": [
    {
      "type": "linkedin_task",
      "delayDays": 0,
      "instructions": "Send a connection request mentioning the webinar.",
      "dueDays": 2
    },
    {
      "delayDays": 3,
      "subjectTemplate": "Hello {{first_name}}",
      "bodyTemplate": "Hi {{first_name}}",
      "threadWithPrevious": false
    },
    {
      "type": "email",
      "delayDays": 1,
      "subjectTemplate": "",
      "bodyTemplate": "Following up.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_3",
    "sequenceId": "eng_seq_3",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_2",
        "position": 1,
        "delayDays": 0,
        "type": "linkedin_task",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "Send a connection request mentioning the webinar.",
        "dueDays": 2
      },
      {
        "id": "eng_stp_3",
        "position": 2,
        "delayDays": 3,
        "type": "email",
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_4",
        "position": 3,
        "delayDays": 1,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Following up.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: task steps in sequences > refuses an email that threads with no earlier email, and task steps without instructions. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 1,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Quick question, {{first_name}}",
        "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 1,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Following up, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: runner with the Nexus transport contract > stores a send without provider ids, and threads only on stored RFC Message-IDs. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 3,
      "subjectTemplate": "Hello {{first_name}}",
      "bodyTemplate": "Hi {{first_name}}",
      "threadWithPrevious": false
    },
    {
      "type": "linkedin_task",
      "delayDays": 0,
      "instructions": "Send a connection request mentioning the webinar.",
      "dueDays": 2
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 3,
        "type": "email",
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 0,
        "type": "linkedin_task",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "Send a connection request mentioning the webinar.",
        "dueDays": 2
      }
    ]
  }
}

Source test: the runner on a task step > not_interested stops the enrollment, records the negative outcome and a suppression ledger event. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 0,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "first_name",
        "sender_name"
      ]
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "whatsapp_template",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0,
        "whatsappTemplate": {
          "templateName": "webinar_invite",
          "language": "en_GB",
          "variables": [
            "first_name",
            "sender_name"
          ]
        }
      }
    ]
  }
}

Source test: the runner on a WhatsApp template step > the last step skipped starts the closing period. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "type": "linkedin_task",
      "delayDays": 1,
      "instructions": "Send a connection request mentioning the webinar.",
      "dueDays": 2
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 1,
        "type": "linkedin_task",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "Send a connection request mentioning the webinar.",
        "dueDays": 2
      }
    ]
  }
}

Source test: the runner on a task step > never renders a task step ahead for approval. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Hi {{first_name}}",
      "bodyTemplate": "Hello.",
      "threadWithPrevious": false
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Hi {{first_name}}",
        "bodyTemplate": "Hello.",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: message approval > a message rendered after a change is approved under the new version; an unstored version falls back to the current one. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Hello {{first_name}}",
      "bodyTemplate": "Hi {{first_name}}",
      "threadWithPrevious": false
    },
    {
      "type": "whatsapp_template",
      "delayDays": 0,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "first_name",
        "sender_name"
      ]
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 0,
        "type": "whatsapp_template",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0,
        "whatsappTemplate": {
          "templateName": "webinar_invite",
          "language": "en_GB",
          "variables": [
            "first_name",
            "sender_name"
          ]
        }
      }
    ]
  }
}

Source test: the runner on a WhatsApp template step > respects the emergency stop, a paused campaign, approval and the touch limit. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 1,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "first_name",
        "sender_name"
      ]
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 1,
        "type": "whatsapp_template",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0,
        "whatsappTemplate": {
          "templateName": "webinar_invite",
          "language": "en_GB",
          "variables": [
            "first_name",
            "sender_name"
          ]
        }
      }
    ]
  }
}

Source test: the runner on a WhatsApp template step > render-ahead leaves WhatsApp steps alone. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 4,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-05T09:00:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Quick question, {{first_name}}",
        "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 4,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Following up, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: render ahead for approval > queues a message due tomorrow today, sends it in the window once approved, and never renders twice. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 0,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "first_name"
      ]
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "whatsapp_template",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0,
        "whatsappTemplate": {
          "templateName": "webinar_invite",
          "language": "en_GB",
          "variables": [
            "first_name"
          ]
        }
      }
    ]
  }
}

Source test: seat WhatsApp number > a WhatsApp step sends from the seat's own number over the workspace connection. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_2",
  "versionId": "eng_seqv_2",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 4,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_2",
    "sequenceId": "eng_seq_2",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Quick question, {{first_name}}",
        "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 4,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Following up, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: enrollment routes > requires an active campaign, a published sequence and a seat. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Hi {{first_name}}",
      "bodyTemplate": "Hello {{first_name}}, see bit.ly/x",
      "threadWithPrevious": false
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Hi {{first_name}}",
        "bodyTemplate": "Hello {{first_name}}, see bit.ly/x",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: sequences.copy_check and publish enforcement > refuses to publish a version with copy errors and allows warnings. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 4,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_2",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Quick question, {{first_name}}",
        "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_3",
        "position": 2,
        "delayDays": 4,
        "type": "email",
        "subjectTemplate": "",
        "bodyTemplate": "Following up, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      }
    ]
  }
}

Source test: sequences.copy_check and publish enforcement > refuses to publish a version with copy errors and allows warnings. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Hello {{first_name}}",
      "bodyTemplate": "Hi {{first_name}}, a short note about the webinar.",
      "threadWithPrevious": false
    },
    {
      "type": "whatsapp_template",
      "delayDays": 1,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "first_name"
      ]
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 0,
        "type": "email",
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}, a short note about the webinar.",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "delayDays": 1,
        "type": "whatsapp_template",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0,
        "whatsappTemplate": {
          "templateName": "webinar_invite",
          "language": "en_GB",
          "variables": [
            "first_name"
          ]
        }
      }
    ]
  }
}

Source test: WhatsApp template steps in sequences > stores the template with its variables, copies it into a new version and gets no email copy findings. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 1,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "first_name"
      ]
    }
  ]
}

Actual local response

{
  "version": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "delayDays": 1,
        "type": "whatsapp_template",
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0,
        "whatsappTemplate": {
          "templateName": "webinar_invite",
          "language": "en_GB",
          "variables": [
            "first_name"
          ]
        }
      }
    ]
  }
}

Source test: WhatsApp template steps in sequences > refuses unknown merge fields, bad template names and languages, and more than ten variables. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Effects and response meaning

  • Atomically deletes and replaces the draft’s steps, assigning new step ids and positions in supplied order. Requires a non-archived sequence and draft version; published versions are immutable.
  • Email templates allow first_name, last_name, company, title and sender_name merge fields. Non-threaded email requires a subject; first step cannot thread and later threading requires a prior email. Saving steps performs these structural checks, not the full publish-time copy QA.
  • Manual LinkedIn/call task steps describe human work; WhatsApp steps reference template/language and ordered variables. Saving these steps is not provider approval, opt-in proof or delivery. Nothing is sent until separately authorized enrollment/runner work.

Verify the result

  • Read sequences.versions.get and compare the complete ordered content and types with the reviewed input. Expect regenerated step ids. Run sequences.copy_check for QA before a separately reviewed publish.

Recover

After an uncertain response, read the exact draft and compare all steps before retrying. Repeating set generates new step ids, and an expected-version guard is absent. Restoring retained prior contents is a separately approved full replacement only while this version is still a draft; published versions require a new draft.

Refused responses

Captured sequences.versions.steps.set · HTTP 422

Action input

{
  "id": "eng_seq_3",
  "versionId": "eng_seqv_3",
  "steps": [
    {
      "type": "linkedin_task",
      "delayDays": 0,
      "instructions": "Send a connection request mentioning the webinar.",
      "dueDays": 2
    },
    {
      "type": "email",
      "delayDays": 1,
      "subjectTemplate": "",
      "bodyTemplate": "Following up.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "error": {
    "code": "invalid_steps",
    "message": "Some steps cannot be saved.",
    "details": {
      "problems": [
        {
          "position": 2,
          "code": "no_email_to_thread"
        }
      ]
    }
  }
}

Source test: task steps in sequences > refuses an email that threads with no earlier email, and task steps without instructions. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Refused input; do not execute

{
  "steps": [
    {
      "type": "call_task",
      "delayDays": 2,
      "instructions": " "
    }
  ],
  "id": "eng_seq_3",
  "versionId": "eng_seqv_3"
}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.versions.steps.set is invalid.",
    "details": [
      {
        "origin": "string",
        "code": "too_small",
        "minimum": 1,
        "inclusive": true,
        "path": [
          "steps",
          0,
          "instructions"
        ],
        "message": "Too small: expected string to have >=1 characters"
      }
    ]
  }
}

Source test: task steps in sequences > refuses an email that threads with no earlier email, and task steps without instructions. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 409

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 4,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "error": {
    "code": "state_conflict",
    "message": "Published versions are immutable; create a new draft version."
  }
}

Source test: sequence routes > creates a sequence with an empty draft, edits steps, publishes and freezes the version. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 404

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_2",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 4,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "error": {
    "code": "not_found",
    "message": "Sequence version was not found."
  }
}

Source test: sequence routes > lists, filters, renames and scopes sequences. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Action input

{
  "id": "eng_seq_3",
  "versionId": "eng_seqv_3",
  "steps": [
    {
      "type": "email",
      "delayDays": 1,
      "subjectTemplate": "",
      "bodyTemplate": "Following up.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "error": {
    "code": "invalid_steps",
    "message": "Some steps cannot be saved.",
    "details": {
      "problems": [
        {
          "position": 1,
          "code": "first_step_cannot_thread"
        }
      ]
    }
  }
}

Source test: task steps in sequences > refuses an email that threads with no earlier email, and task steps without instructions. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Refused input; do not execute

{
  "steps": [
    {
      "type": "call_task",
      "delayDays": 2,
      "instructions": "  Call the main office.  ",
      "subjectTemplate": "x"
    }
  ],
  "id": "eng_seq_3",
  "versionId": "eng_seqv_3"
}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.versions.steps.set is invalid.",
    "details": [
      {
        "code": "unrecognized_keys",
        "keys": [
          "subjectTemplate"
        ],
        "path": [
          "steps",
          0
        ],
        "message": "Unrecognized key: \"subjectTemplate\""
      }
    ]
  }
}

Source test: task steps in sequences > refuses an email that threads with no earlier email, and task steps without instructions. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Hi",
      "bodyTemplate": "Hello {{nickname}}",
      "threadWithPrevious": true
    },
    {
      "delayDays": 2,
      "subjectTemplate": "",
      "bodyTemplate": "Again",
      "threadWithPrevious": false
    }
  ]
}

Actual local response

{
  "error": {
    "code": "invalid_steps",
    "message": "Some steps cannot be saved.",
    "details": {
      "problems": [
        {
          "position": 1,
          "code": "first_step_cannot_thread"
        },
        {
          "position": 1,
          "code": "unknown_merge_field",
          "field": "nickname"
        },
        {
          "position": 2,
          "code": "subject_required"
        }
      ]
    }
  }
}

Source test: sequence routes > validates steps: no threading on step 1, subjects unless threaded, known merge fields, at least one step to publish. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Refused input; do not execute

{
  "steps": [],
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.versions.steps.set is invalid.",
    "details": [
      {
        "origin": "array",
        "code": "too_small",
        "minimum": 1,
        "inclusive": true,
        "path": [
          "steps"
        ],
        "message": "Too small: expected array to have >=1 items"
      }
    ]
  }
}

Source test: sequence routes > validates steps: no threading on step 1, subjects unless threaded, known merge fields, at least one step to publish. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 409

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1",
  "steps": [
    {
      "delayDays": 0,
      "subjectTemplate": "Quick question, {{first_name}}",
      "bodyTemplate": "Hi {{first_name}},\n\nHow is {{company}} handling outreach?\n\n{{sender_name}}",
      "threadWithPrevious": false
    },
    {
      "delayDays": 4,
      "subjectTemplate": "",
      "bodyTemplate": "Following up, {{first_name}}.",
      "threadWithPrevious": true
    }
  ]
}

Actual local response

{
  "error": {
    "code": "state_conflict",
    "message": "Archived sequences cannot be changed."
  }
}

Source test: sequence routes > allows one live sequence per campaign and refuses archived campaigns. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Refused input; do not execute

{
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 1,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "phone"
      ]
    }
  ],
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.versions.steps.set is invalid.",
    "details": [
      {
        "code": "invalid_union",
        "errors": [
          [
            {
              "code": "invalid_value",
              "values": [
                "email"
              ],
              "path": [
                "type"
              ],
              "message": "Invalid input: expected \"email\""
            },
            {
              "expected": "string",
              "code": "invalid_type",
              "path": [
                "subjectTemplate"
              ],
              "message": "Invalid input: expected string, received undefined"
            },
            {
              "expected": "string",
              "code": "invalid_type",
              "path": [
                "bodyTemplate"
              ],
              "message": "Invalid input: expected string, received undefined"
            },
            {
              "expected": "boolean",
              "code": "invalid_type",
              "path": [
                "threadWithPrevious"
              ],
              "message": "Invalid input: expected boolean, received undefined"
            },
            {
              "code": "unrecognized_keys",
              "keys": [
                "templateName",
                "language",
                "variables"
              ],
              "path": [],
              "message": "Unrecognized keys: \"templateName\", \"language\", \"variables\""
            }
          ],
          [
            {
              "code": "invalid_value",
              "values": [
                "linkedin_task",
                "call_task"
              ],
              "path": [
                "type"
              ],
              "message": "Invalid option: expected one of \"linkedin_task\"|\"call_task\""
            },
            {
              "expected": "string",
              "code": "invalid_type",
              "path": [
                "instructions"
              ],
              "message": "Invalid input: expected string, received undefined"
            },
            {
              "code": "unrecognized_keys",
              "keys": [
                "templateName",
                "language",
                "variables"
              ],
              "path": [],
              "message": "Unrecognized keys: \"templateName\", \"language\", \"variables\""
            }
          ],
          [
            {
              "code": "invalid_value",
              "values": [
                "first_name",
                "last_name",
                "company",
                "title",
                "sender_name"
              ],
              "path": [
                "variables",
                0
              ],
              "message": "Invalid option: expected one of \"first_name\"|\"last_name\"|\"company\"|\"title\"|\"sender_name\""
            }
          ]
        ],
        "path": [
          "steps",
          0
        ],
        "message": "Invalid input"
      }
    ]
  }
}

Source test: WhatsApp template steps in sequences > refuses unknown merge fields, bad template names and languages, and more than ten variables. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Refused input; do not execute

{
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 1,
      "templateName": "Webinar Invite",
      "language": "en_GB",
      "variables": [
        "first_name"
      ]
    }
  ],
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.versions.steps.set is invalid.",
    "details": [
      {
        "origin": "string",
        "code": "invalid_format",
        "format": "regex",
        "pattern": "/^[a-z0-9_]{1,512}$/",
        "path": [
          "steps",
          0,
          "templateName"
        ],
        "message": "Invalid string: must match pattern /^[a-z0-9_]{1,512}$/"
      }
    ]
  }
}

Source test: WhatsApp template steps in sequences > refuses unknown merge fields, bad template names and languages, and more than ten variables. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Refused input; do not execute

{
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 1,
      "templateName": "webinar_invite",
      "language": "english",
      "variables": [
        "first_name"
      ]
    }
  ],
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.versions.steps.set is invalid.",
    "details": [
      {
        "origin": "string",
        "code": "invalid_format",
        "format": "regex",
        "pattern": "/^[a-z]{2,3}(_[A-Z]{2})?$/",
        "path": [
          "steps",
          0,
          "language"
        ],
        "message": "Invalid string: must match pattern /^[a-z]{2,3}(_[A-Z]{2})?$/"
      }
    ]
  }
}

Source test: WhatsApp template steps in sequences > refuses unknown merge fields, bad template names and languages, and more than ten variables. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Refused input; do not execute

{
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 1,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "first_name",
        "first_name",
        "first_name",
        "first_name",
        "first_name",
        "first_name",
        "first_name",
        "first_name",
        "first_name",
        "first_name",
        "first_name"
      ]
    }
  ],
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.versions.steps.set is invalid.",
    "details": [
      {
        "origin": "array",
        "code": "too_big",
        "maximum": 10,
        "inclusive": true,
        "path": [
          "steps",
          0,
          "variables"
        ],
        "message": "Too big: expected array to have <=10 items"
      }
    ]
  }
}

Source test: WhatsApp template steps in sequences > refuses unknown merge fields, bad template names and languages, and more than ten variables. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.versions.steps.set · HTTP 422

Refused input; do not execute

{
  "steps": [
    {
      "type": "whatsapp_template",
      "delayDays": 1,
      "templateName": "webinar_invite",
      "language": "en_GB",
      "variables": [
        "first_name"
      ],
      "subjectTemplate": "x"
    }
  ],
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.versions.steps.set is invalid.",
    "details": [
      {
        "code": "unrecognized_keys",
        "keys": [
          "subjectTemplate"
        ],
        "path": [
          "steps",
          0
        ],
        "message": "Unrecognized key: \"subjectTemplate\""
      }
    ]
  }
}

Source test: WhatsApp template steps in sequences > refuses unknown merge fields, bad template names and languages, and more than ten variables. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Error recovery

  • HTTP 403 permission_denied: Stop and inspect the permission required by this action in the current credential. Do not broaden permissions or switch organization automatically.
  • HTTP 422 invalid_action_input: Correct required fields, enums and nested step bounds against the current input schema, then validate the exact intended payload again.
  • HTTP 422 invalid_steps: Inspect details.problems and the current step schema. Correct the exact draft before validating again; a version must contain steps to publish, thread rules must hold and unknown merge fields are refused.
  • HTTP 409 state_conflict: Read sequences.get and the exact version. Archived sequences cannot change, only one non-archived sequence per campaign and one draft per sequence may exist, and published versions cannot be edited. Re-plan only a legal, separately approved operation.
  • HTTP 404 not_found: Rediscover the sequence using sequences.list in the same workspace and campaign; use status=archived when needed. Discover versionId from sequences.get and confirm it belongs to the same sequence. Do not guess another workspace or version.

Evidence and limits

  • contracts/sequences.mjs

  • apps/api/src/sequences/sequences-handlers.ts

  • apps/api/src/sequences/sequences-domain.ts

  • apps/api/src/sequences/sequences-repository.ts

  • apps/api/src/copy-qa/version-steps.ts

  • apps/api/src/copy-qa/copy-qa.ts

  • Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

  • Widget/bootstrap use actual app handler but mocked worker runtime and app identity.

  • No gateway execution, live permissions, external delivery or deployed proof.

The examples are local route captures with synthetic context and simulated providers. They do not prove live authorization, CLI/MCP gateway execution, production state or delivery.