Topolo Docs Topolo Platform
Open workspace

Create sequence

Create a campaign sequence with an empty draft version 1.

Outcome

Create a campaign sequence with an empty draft version 1.

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 campaignId through campaigns.list and campaigns.get in the selected workspace. Confirm the campaign is not archived, then inspect sequences.list campaignId before creating.
topolo whoami --json
topolo actions get app_topolo_engage.sequences.create --json
topolo workspaces list topolo-engage --json

Input fields

FieldRequiredMeaning and constraints
campaignIdYesRequired non-archived campaign identifier discovered in this workspace, trimmed 1–128 characters.
nameYesRequired trimmed non-empty sequence name up to 120 characters; it is not an idempotency key.

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.create --data @payload.json --json
topolo actions plan app_topolo_engage.sequences.create --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.create --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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_1",
  "name": "Main"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_2",
  "name": "Main"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_2",
    "campaignId": "eng_cmp_2",
    "name": "Main",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_2",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_2",
    "sequenceId": "eng_seq_2",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_3",
  "name": "Main"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_3",
    "campaignId": "eng_cmp_3",
    "name": "Main",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_3",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_3",
    "sequenceId": "eng_seq_3",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_2",
  "name": "Main"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_3",
    "campaignId": "eng_cmp_2",
    "name": "Main",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_3",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_3",
    "sequenceId": "eng_seq_3",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_legacy",
  "name": "Main"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_legacy",
    "name": "Main",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_1",
  "name": "Main"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "draft",
    "createdAt": "2026-10-05T09:00:00.000Z",
    "updatedAt": "2026-10-05T09:00:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-05T09:00:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_1",
  "name": "Unpublished"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Unpublished",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_1",
  "name": "Again"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_3",
    "campaignId": "eng_cmp_1",
    "name": "Again",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_3",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_3",
    "sequenceId": "eng_seq_3",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_1",
  "name": "One"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "One",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_2",
  "name": "Two"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_2",
    "campaignId": "eng_cmp_2",
    "name": "Two",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_2",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_2",
    "sequenceId": "eng_seq_2",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

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.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_1",
  "name": "x"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_2",
    "campaignId": "eng_cmp_1",
    "name": "x",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_2",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_2",
    "sequenceId": "eng_seq_2",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

Source test: sequence routes > rethrows unexpected storage errors when inserting. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.create · HTTP 201

Action input

{
  "campaignId": "eng_cmp_1",
  "name": "x"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "x",
    "status": "draft",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "draft",
      "publishedAt": null
    }
  ],
  "draft": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "draft",
    "publishedAt": null,
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": []
  },
  "published": null
}

Source test: sequence routes > reports a lost publish race as a conflict. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Effects and response meaning

  • Atomically creates a draft sequence plus empty draft version 1. Returns sequence detail with HTTP 201. A campaign may have at most one non-archived sequence; a conflicting creation is refused.
  • No steps are sent or contacts enrolled. No idempotency key exists; after a lost response, a conflict means a current sequence exists, not that its content matches the intended request.

Verify the result

  • Read sequences.get using returned sequence.id. Compare campaignId/name and confirm draft.version=1, draft.status=draft and empty steps before a separately approved steps.set.

Recover

After an uncertain response, list sequences for the exact campaign and read the discovered sequence. Compare identity, name and draft before retrying. Never archive an existing sequence to bypass a conflict automatically; archiving has separate lifecycle consequences and requires approval.

Refused responses

Captured sequences.create · HTTP 409

Action input

{
  "campaignId": "eng_cmp_1",
  "name": "Second"
}

Actual local response

{
  "error": {
    "code": "state_conflict",
    "message": "This campaign already has a sequence; archive it first."
  }
}

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.create · HTTP 404

Action input

{
  "campaignId": "eng_cmp_x",
  "name": "X"
}

Actual local response

{
  "error": {
    "code": "not_found",
    "message": "Campaign was not found in this workspace."
  }
}

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.create · HTTP 403

Action input

{
  "campaignId": "x",
  "name": "x"
}

Actual local response

{
  "error": {
    "code": "permission_denied",
    "message": "The credential lacks workspace:write.",
    "details": {
      "requiredPermission": "workspace:write"
    }
  }
}

Source test: sequence routes > enforces permissions and input schemas. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.create · HTTP 422

Refused input; do not execute

{}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.create is invalid.",
    "details": []
  }
}

Source test: sequence routes > enforces permissions and input schemas. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured sequences.create · HTTP 409

Action input

{
  "campaignId": "eng_cmp_2",
  "name": "X"
}

Actual local response

{
  "error": {
    "code": "state_conflict",
    "message": "Archived campaigns cannot get a sequence."
  }
}

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.create · HTTP 422

Refused input; do not execute

{
  "name": "x"
}

Actual local response

{
  "error": {
    "code": "invalid_action_input",
    "message": "Input for sequences.create is invalid.",
    "details": [
      {
        "expected": "string",
        "code": "invalid_type",
        "path": [
          "campaignId"
        ],
        "message": "Invalid input: expected string, received undefined"
      }
    ]
  }
}

Source test: sequence routes > enforces permissions and input schemas. 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 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.