Topolo Docs Topolo Platform
Open workspace

Publish sequence version

Publish a reviewed draft as immutable content for new enrollments.

Outcome

Publish a reviewed draft as immutable content for new enrollments.

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 its complete ordered steps. Run sequences.copy_check for that saved version and review all findings before publication.
topolo whoami --json
topolo actions get app_topolo_engage.sequences.versions.publish --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.

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

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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,
        "type": "email",
        "delayDays": 4,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "whatsapp_template",
        "delayDays": 0,
        "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,
        "type": "email",
        "delayDays": 2,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "linkedin_task",
        "delayDays": 0,
        "subjectTemplate": "",
        "bodyTemplate": "",
        "threadWithPrevious": false,
        "instructions": "Send a connection request mentioning the webinar.",
        "dueDays": 2
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "type": "email",
        "delayDays": 3,
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_3",
        "position": 3,
        "type": "call_task",
        "delayDays": 2,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_2",
  "versionId": "eng_seqv_2"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_2",
    "campaignId": "eng_cmp_2",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_2",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_2",
    "sequenceId": "eng_seq_2",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_3",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_3",
  "versionId": "eng_seqv_3"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_3",
    "campaignId": "eng_cmp_3",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_3",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_3",
    "sequenceId": "eng_seq_3",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_4",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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,
        "type": "email",
        "delayDays": 4,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_2",
  "versionId": "eng_seqv_2"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_2",
    "campaignId": "eng_cmp_2",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_2",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_2",
    "sequenceId": "eng_seq_2",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_3",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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,
        "type": "email",
        "delayDays": 4,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 3,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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,
        "type": "email",
        "delayDays": 1,
        "subjectTemplate": "",
        "bodyTemplate": "Following up, {{first_name}}.",
        "threadWithPrevious": true,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_3",
        "position": 3,
        "type": "email",
        "delayDays": 1,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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,
        "type": "email",
        "delayDays": 1,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 3,
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "type": "linkedin_task",
        "delayDays": 0,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "whatsapp_template",
        "delayDays": 0,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "linkedin_task",
        "delayDays": 1,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_legacy",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "subjectTemplate": "Hello {{first_name}}",
        "bodyTemplate": "Hi {{first_name}}",
        "threadWithPrevious": false,
        "instructions": "",
        "dueDays": 0
      },
      {
        "id": "eng_stp_2",
        "position": 2,
        "type": "whatsapp_template",
        "delayDays": 0,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "whatsapp_template",
        "delayDays": 1,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-05T09:00:00.000Z",
    "updatedAt": "2026-10-05T09:00:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-05T09:00:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-05T09:00:00.000Z",
    "createdAt": "2026-10-05T09:00:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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,
        "type": "email",
        "delayDays": 4,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "whatsapp_template",
        "delayDays": 0,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_2",
  "versionId": "eng_seqv_2"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_2",
    "campaignId": "eng_cmp_2",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_2",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_2",
    "sequenceId": "eng_seq_2",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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,
        "type": "email",
        "delayDays": 4,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_2",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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,
        "type": "email",
        "delayDays": 4,
        "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.publish · HTTP 200

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "sequence": {
    "id": "eng_seq_1",
    "campaignId": "eng_cmp_1",
    "name": "Main",
    "status": "active",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  },
  "versions": [
    {
      "id": "eng_seqv_1",
      "version": 1,
      "status": "published",
      "publishedAt": "2026-10-06T08:30:00.000Z"
    }
  ],
  "draft": null,
  "published": {
    "id": "eng_seqv_1",
    "sequenceId": "eng_seq_1",
    "version": 1,
    "status": "published",
    "publishedAt": "2026-10-06T08:30:00.000Z",
    "createdAt": "2026-10-06T08:30:00.000Z",
    "steps": [
      {
        "id": "eng_stp_1",
        "position": 1,
        "type": "email",
        "delayDays": 0,
        "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,
        "type": "whatsapp_template",
        "delayDays": 1,
        "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.

Effects and response meaning

  • Requires non-empty draft steps with no error-severity copy QA findings; warnings do not block. Marks the exact version published with publishedAt, then makes sequence active. New enrollments pin this published content; existing enrollments keep their earlier pins.
  • Version publication and sequence activation are separate database writes. After an interrupted operation, inspect both version.status and sequence.status rather than treating a published version alone as complete activation. No message is sent by publication itself.

Verify the result

  • Read sequences.versions.get and sequences.get. Confirm exact version published with publishedAt and sequence active; compare immutable step contents to the reviewed draft. Check both writes after an uncertain response.

Recover

After an uncertain response, read the exact version and sequence. Do not repeat publish against an already published version: it is no longer a draft. If version is published but sequence activation is incomplete, stop and report the inconsistent state; this API exposes no supported activation repair. Published content cannot be rolled back in place; a separately reviewed new draft/publication affects future enrollments, not existing pins.

Refused responses

Captured sequences.versions.publish · HTTP 409

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

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

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "error": {
    "code": "invalid_steps",
    "message": "Add at least one step before publishing.",
    "details": {
      "problems": []
    }
  }
}

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

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "error": {
    "code": "copy_qa_failed",
    "message": "Fix the copy errors before publishing; warnings do not block.",
    "details": {
      "pass": false,
      "errorCount": 1,
      "warningCount": 3,
      "steps": [
        {
          "position": 1,
          "findings": [
            {
              "code": "merge_field_without_fallback",
              "severity": "warning",
              "message": "{{first_name}} is optional contact data and has no fallback: contacts without it stop instead of receiving this step.",
              "location": {
                "field": "subject",
                "match": "{{first_name}}"
              }
            },
            {
              "code": "body_too_short",
              "severity": "warning",
              "message": "4 words; cold outreach under 25 words often lacks context.",
              "location": {
                "field": "body"
              }
            },
            {
              "code": "merge_field_without_fallback",
              "severity": "warning",
              "message": "{{first_name}} is optional contact data and has no fallback: contacts without it stop instead of receiving this step.",
              "location": {
                "field": "body",
                "match": "{{first_name}}"
              }
            },
            {
              "code": "link_shortener",
              "severity": "error",
              "message": "\"bit.ly\" is a link shortener; mailbox providers distrust them. Link to the destination directly.",
              "location": {
                "field": "body",
                "match": "bit.ly/x"
              }
            }
          ]
        }
      ]
    }
  }
}

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.publish · HTTP 409

Action input

{
  "id": "eng_seq_1",
  "versionId": "eng_seqv_1"
}

Actual local response

{
  "error": {
    "code": "state_conflict",
    "message": "This version was already published."
  }
}

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.

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 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 422 copy_qa_failed: Inspect the returned QA report, repair error-severity findings in the draft, run sequences.copy_check again and read the draft before another reviewed publish. Warnings do not block; passing QA alone does not approve outreach.

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.