Outcome
Change campaign status along its declared transition graph.
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 and read the campaign in the same workspace before selecting its next status.
- Allowed transitions: draft→active|archived; active→paused|completed|archived; paused→active|completed|archived; completed→archived; archived has no outgoing transition.
topolo whoami --json
topolo actions get app_topolo_engage.campaigns.transition --json
topolo workspaces list topolo-engage --json
Input fields
| Field | Required | Meaning and constraints |
|---|---|---|
id | Yes | Required campaign identifier discovered in this workspace. |
status | Yes | Required target: draft, active, paused, completed or archived. Schema acceptance does not mean the current state permits that transition. |
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.campaigns.transition --data @payload.json --json
topolo actions plan app_topolo_engage.campaigns.transition --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.campaigns.transition --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 campaigns.transition · HTTP 200
Action input
{
"id": "eng_cmp_1",
"status": "active"
}
Actual local response
{
"campaign": {
"id": "eng_cmp_1",
"name": "Q4",
"goal": null,
"channel": "email",
"status": "active",
"isTest": false,
"createdBy": "user_1",
"updatedBy": "user_1",
"createdAt": "2026-10-06T08:30:00.000Z",
"updatedAt": "2026-10-06T08:30:00.000Z",
"archivedAt": 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.
Effects and response meaning
- Updates status, updatedBy and updatedAt. A transition to archived also records archivedAt and removes the campaign from the default list.
- Archiving is terminal; no unarchive action is declared. Review that consequence before approval.
- Activating a campaign changes a prerequisite evaluated by the sender runner; this response is not proof that recipients were eligible, messages were sent or providers delivered them.
Verify the result
- Read campaigns.get and compare campaign.status to the approved target. For archived, verify archivedAt and rediscover with campaigns.list status=archived.
Recover
After an uncertain response, read campaigns.get before any retry. If the target status is already present, do not repeat the transition: a same-status request is not a legal transition. A paused campaign can return to active, and an active campaign can be paused, but each compensating move requires fresh review. Completed and archived states cannot return to active through this graph.
Refused responses
Captured campaigns.transition · HTTP 404
Action input
{
"id": "eng_cmp_1",
"status": "active"
}
Actual local response
{
"error": {
"code": "not_found",
"message": "Campaign was not found in this workspace."
}
}
Source test: campaign routes > returns 404 for a campaign that lives in another workspace. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Captured campaigns.transition · HTTP 409
Action input
{
"id": "eng_cmp_1",
"status": "paused"
}
Actual local response
{
"error": {
"code": "state_conflict",
"message": "A draft campaign cannot move to paused."
}
}
Source test: campaign routes > returns 409 for an invalid transition and for editing an archived campaign. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Error recovery
- HTTP 404
not_found: Rediscover the campaign using campaigns.list in the same authorized workspace, including status=archived when appropriate. Do not guess another organization or workspace. - HTTP 409
state_conflict: Read campaigns.get and compare current status with the transition graph. Do not repeatedly request an illegal or already-applied transition; re-plan only a still-valid authorized move.
Related actions
Evidence and limits
-
contracts/campaigns.mjs -
apps/api/src/campaigns/campaigns-routes.ts -
apps/api/src/campaigns/campaigns-handlers.ts -
apps/api/src/campaigns/campaigns-domain.ts -
apps/api/src/campaigns/campaigns-repository.ts -
apps/api/test/campaigns-routes.test.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.