Topolo Docs Topolo Platform
Open workspace

Reject message

Reject a pending message and stop its enrollment.

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

FieldRequiredMeaning and constraints
idYesRequired pending message identifier discovered through approvals.list.
reasonYesRequired 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.

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.