Outcome
Reject a pending message and stop its enrollment.
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
- Read approvals.list in the intended authorized Engage workspace. Select messages[].id and retain that row’s exact subject, text, version, campaignId, enrollmentId, stepId, batchDate and scheduledFor.
- Read campaigns.policy.get using the discovered campaignId to inspect the current policy and workspace stop. Bind workspace identity separately from action input.
topolo whoami --json
topolo actions get app_topolo_engage.approvals.reject --json
topolo workspaces list topolo-engage --json
Input fields
| Field | Required | Meaning and constraints |
|---|---|---|
id | Yes | Required pending message identifier discovered through approvals.list. |
reason | Yes | Required trimmed rejection explanation, 1–500 characters; retained as decision evidence. |
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.approvals.reject --data @payload.json --json
topolo actions plan app_topolo_engage.approvals.reject --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.approvals.reject --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 approvals.reject · HTTP 200
Action input
{
"id": "eng_msg_1",
"reason": "Wrong tone"
}
Actual local response
{
"message": {
"id": "eng_msg_1",
"campaignId": "eng_cmp_1",
"enrollmentId": "eng_enr_1",
"stepId": "eng_stp_1",
"stepPosition": 1,
"version": 1,
"previousMessageId": null,
"subject": "Quick question, Ada",
"text": "Hi Ada,\n\nHow is Analytical handling outreach?\n\nAsh\n\n--\nTest opt-out sentence for fixtures.\nExample Ltd\n1 Example Street\nLondon",
"status": "cancelled",
"approvalState": "rejected",
"policyVersion": 1,
"batchDate": "2026-10-06",
"approvedBy": "user_1",
"approvedAt": "2026-10-06T08:30:00.000Z",
"approvalNote": "Wrong tone",
"scheduledFor": "2026-10-06T08:30:00.000Z",
"footerSource": "manual",
"brandKitRef": null,
"brandVersionRef": null
}
}
Source test: message approval > reject cancels the message, stops its enrollment and is audited. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Effects and response meaning
- Sets the pending message to cancelled and rejected while retaining its rendered content and decision evidence.
- Stops its enrollment with message_rejected, preventing later sequence steps for that enrollment. Records an audit entry.
- Only a scheduled pending-approval message can be rejected; this action cannot recall approved or sent messages.
Verify the result
- Read enrollments.messages.list with id=the retained enrollmentId in the same workspace. Match the specific messages[].id and inspect approvalState, status, holdReason, scheduledFor, sentAt and transportMessageId.
- A message leaving approvals.list only proves it is no longer pending; it does not by itself establish approval, delivery, rejection or deletion.
- Read enrollments.get using retained enrollmentId and verify status=stopped and stopReason=message_rejected. Match response message.id with the rejected pending version.
Recover
After a lost response, read message history and enrollment state before another decision. A rejected message is no longer pending, and no un-reject action is declared. Never interpret state_conflict alone as evidence that this particular rejection succeeded. Re-enrollment would be a separate reviewed operation with its own recipient and send effects.
Refused responses
Captured approvals.reject · HTTP 409
Action input
{
"id": "eng_msg_1",
"reason": "again"
}
Actual local response
{
"error": {
"code": "state_conflict",
"message": "This message is not waiting for approval."
}
}
Source test: message approval > reject cancels the message, stops its enrollment and is audited. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Captured approvals.reject · HTTP 422
Refused input; do not execute
{
"id": "eng_msg_1"
}
Actual local response
{
"error": {
"code": "invalid_action_input",
"message": "Input for approvals.reject is invalid.",
"details": [
{
"expected": "string",
"code": "invalid_type",
"path": [
"reason"
],
"message": "Invalid input: expected string, received undefined"
}
]
}
}
Source test: message approval > reject cancels the message, stops its enrollment and is audited. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Captured approvals.reject · HTTP 409
Action input
{
"id": "eng_msg_1",
"reason": "x"
}
Actual local response
{
"error": {
"code": "state_conflict",
"message": "This message is not waiting for approval."
}
}
Source test: message approval > 409s a lost rejection race. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Error recovery
- HTTP 403
permission_denied: Stop and inspect the action permission in the current credential; do not broaden permissions or switch contexts automatically. - HTTP 422
invalid_action_input: Correct the input against the current schema and validate again before planning. - HTTP 409
state_conflict: Read current message history and campaign policy. A message must still be scheduled and pending_approval; batch approval also requires the per_batch policy. Never loop on an already-applied or invalid decision.
Related actions
Evidence and limits
-
contracts/autonomy.mjs -
contracts/sending.mjs -
apps/api/src/autonomy/approvals-handlers.ts -
apps/api/src/autonomy/approvals-repository.ts -
apps/api/src/autonomy/policy-service.ts -
apps/api/test/autonomy.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.