Outcome
Inspect the exact rendered messages currently waiting for approval.
Before you start
- Verify the environment, credential, authorized organization and the published action permission workspace:read.
- Discover an accessible Engage workspace and bind resourceType=workspace and its resourceId separately from action input. Fixture identifiers grant no access.
Discover the target
- Discover and select an authorized Engage workspace. The read requires no message identifier.
- Optionally filter with campaignId returned by campaigns.list. The queue is limited to 500 messages and has no declared continuation cursor.
topolo whoami --json
topolo actions get app_topolo_engage.approvals.list --json
topolo workspaces list topolo-engage --json
Input fields
| Field | Required | Meaning and constraints |
|---|---|---|
campaignId | No | Optional campaign identifier discovered in this workspace. Omit to read the workspace queue, up to 500 pending messages. |
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.list --data @payload.json --json
topolo actions plan app_topolo_engage.approvals.list --resource-type workspace --resource-id "$ENGAGE_WORKSPACE_ID" --data @payload.json --json
Execute the unchanged validated read in the selected workspace.
: "${ENGAGE_WORKSPACE_ID:?Set ENGAGE_WORKSPACE_ID to your selected workspace identifier}"
topolo actions call app_topolo_engage.approvals.list --resource-type workspace --resource-id "$ENGAGE_WORKSPACE_ID" --data @payload.json --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.list · HTTP 200
Action input
{}
Actual local response
{
"messages": [
{
"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": "scheduled",
"approvalState": "pending_approval",
"policyVersion": 1,
"batchDate": "2026-10-06",
"approvedBy": null,
"approvedAt": null,
"approvalNote": null,
"scheduledFor": "2026-10-06T08:30:00.000Z",
"footerSource": "manual",
"brandKitRef": null,
"brandVersionRef": null
}
],
"workspace": {
"emergencyStop": false,
"reason": null,
"updatedBy": null,
"updatedAt": null
}
}
Source test: message approval > the runner never sends an unapproved message: it parks it for approval, then sends it once approved. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Captured approvals.list · HTTP 200
Action input
{}
Actual local response
{
"messages": [
{
"id": "eng_msg_3",
"campaignId": "eng_cmp_1",
"enrollmentId": "eng_enr_1",
"stepId": "eng_stp_1",
"stepPosition": 1,
"version": 3,
"previousMessageId": "eng_msg_2",
"subject": "New subject",
"text": "Final text.",
"status": "scheduled",
"approvalState": "pending_approval",
"policyVersion": 1,
"batchDate": "2026-10-06",
"approvedBy": null,
"approvedAt": null,
"approvalNote": null,
"scheduledFor": "2026-10-06T08:30:00.000Z",
"footerSource": "manual",
"brandKitRef": null,
"brandVersionRef": null
}
],
"workspace": {
"emergencyStop": false,
"reason": null,
"updatedBy": null,
"updatedAt": null
}
}
Source test: message approval > request_edit creates a new immutable version with its own idempotency key and keeps the previous one unchanged. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Captured approvals.list · HTTP 200
Action input
{}
Actual local response
{
"messages": [],
"workspace": {
"emergencyStop": true,
"reason": "Incident",
"updatedBy": "user_1",
"updatedAt": "2026-10-06T08:30:00.000Z"
}
}
Source test: emergency stop and seat allowlist > the workspace stop holds every campaign until resumed. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Captured approvals.list · HTTP 200
Action input
{}
Actual local response
{
"messages": [
{
"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": "scheduled",
"approvalState": "pending_approval",
"policyVersion": 1,
"batchDate": "2026-10-05",
"approvedBy": null,
"approvedAt": null,
"approvalNote": null,
"scheduledFor": "2026-10-06T07:00:00.000Z",
"footerSource": "manual",
"brandKitRef": null,
"brandVersionRef": null
}
],
"workspace": {
"emergencyStop": false,
"reason": null,
"updatedBy": null,
"updatedAt": 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 approvals.list · HTTP 200
Action input
{}
Actual local response
{
"messages": [],
"workspace": {
"emergencyStop": false,
"reason": null,
"updatedBy": null,
"updatedAt": null
}
}
Source test: render ahead for approval > fails closed: an incomplete footer or an unresolved merge field is held with its reason, never queued. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Captured approvals.list · HTTP 200
Action input
{
"campaignId": "eng_cmp_other"
}
Actual local response
{
"messages": [],
"workspace": {
"emergencyStop": false,
"reason": null,
"updatedBy": null,
"updatedAt": null
}
}
Source test: message approval > the runner never sends an unapproved message: it parks it for approval, then sends it once approved. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Effects and response meaning
- Returns messages[] ordered by campaign, step position, batch date and id, plus the workspace emergency-stop state. Only scheduled pending-approval rows are included.
- Read exact subject and text, footer provenance and policyVersion before any decision. An empty queue is not a complete history of approved, rejected or sent messages.
Verify the result
- Verify every returned row is the intended campaign, version and recipient enrollment before choosing an action. Retain messages[].enrollmentId to read subsequent message history.
Recover
Retry the read in the same verified context after a transient failure. When a retained message disappears, read enrollments.messages.list rather than guessing why it left the queue.
Refused responses
No refusal response for this action is included in this fixture set. The recovery guidance below is based on the route’s declared permission and input-validation checks.
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.
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.