Outcome
Replace a pending message with a new immutable version for fresh approval.
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.request_edit --json
topolo workspaces list topolo-engage --json
Input fields
| Field | Required | Meaning and constraints |
|---|---|---|
id | Yes | Required current pending message id discovered through approvals.list. |
subject | No | Optional trimmed replacement subject, 1–300 characters. Omit to retain the prior subject. |
text | Yes | Required trimmed complete replacement body, 1–20,000 characters. Review the final text and compliance footer. |
note | No | Optional trimmed edit explanation, 1–500 characters. |
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.request_edit --data @payload.json --json
topolo actions plan app_topolo_engage.approvals.request_edit --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.request_edit --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.request_edit · HTTP 200
Action input
{
"id": "eng_msg_1",
"text": "Hi Ada, shorter.",
"note": "Too long"
}
Actual local response
{
"message": {
"id": "eng_msg_2",
"campaignId": "eng_cmp_1",
"enrollmentId": "eng_enr_1",
"stepId": "eng_stp_1",
"stepPosition": 1,
"version": 2,
"previousMessageId": "eng_msg_1",
"subject": "Quick question, Ada",
"text": "Hi Ada, shorter.",
"status": "scheduled",
"approvalState": "pending_approval",
"policyVersion": 1,
"batchDate": "2026-10-06",
"approvedBy": null,
"approvedAt": null,
"approvalNote": "Too long",
"scheduledFor": "2026-10-06T08:30:00.000Z",
"footerSource": "manual",
"brandKitRef": null,
"brandVersionRef": null
},
"previous": {
"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": "superseded",
"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
}
}
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.request_edit · HTTP 200
Action input
{
"id": "eng_msg_1",
"text": "Edited body"
}
Actual local response
{
"message": {
"id": "eng_msg_2",
"campaignId": "eng_cmp_1",
"enrollmentId": "eng_enr_1",
"stepId": "eng_stp_1",
"stepPosition": 1,
"version": 2,
"previousMessageId": "eng_msg_1",
"subject": "Quick question, Ada",
"text": "Edited body",
"status": "scheduled",
"approvalState": "pending_approval",
"policyVersion": 1,
"batchDate": "2026-10-05",
"approvedBy": null,
"approvedAt": null,
"approvalNote": null,
"scheduledFor": "2026-10-05T09:00:00.000Z",
"footerSource": "brand",
"brandKitRef": "bk_1",
"brandVersionRef": "bkv_1"
},
"previous": {
"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--\nBrand v1 opt-out.\nBrand Ltd\n1 Old Road",
"status": "cancelled",
"approvalState": "superseded",
"policyVersion": 1,
"batchDate": "2026-10-05",
"approvedBy": null,
"approvedAt": null,
"approvalNote": null,
"scheduledFor": "2026-10-06T07:00:00.000Z",
"footerSource": "brand",
"brandKitRef": "bk_1",
"brandVersionRef": "bkv_1"
}
}
Source test: runner with a footer connected to Brand > an edited pending message keeps the footer provenance of the version it supersedes. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.
Captured approvals.request_edit · HTTP 200
Action input
{
"id": "eng_msg_2",
"subject": "New subject",
"text": "Final text."
}
Actual local response
{
"message": {
"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
},
"previous": {
"id": "eng_msg_2",
"campaignId": "eng_cmp_1",
"enrollmentId": "eng_enr_1",
"stepId": "eng_stp_1",
"stepPosition": 1,
"version": 2,
"previousMessageId": "eng_msg_1",
"subject": "Quick question, Ada",
"text": "Hi Ada, shorter.",
"status": "cancelled",
"approvalState": "superseded",
"policyVersion": 1,
"batchDate": "2026-10-06",
"approvedBy": null,
"approvedAt": null,
"approvalNote": "Too long",
"scheduledFor": "2026-10-06T08:30:00.000Z",
"footerSource": "manual",
"brandKitRef": null,
"brandVersionRef": 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.
Effects and response meaning
- Atomically supersedes the old pending version and inserts a new pending message with a new id, incremented version, previousMessageId and versioned send idempotency key. The old rendered content is retained read-only.
- The supplied text replaces the body. Omitted subject retains the prior subject; this action does not approve the replacement.
- The replacement retains its enrollment, step, batch date and footer provenance. Do not assume the edited body has a regenerated compliance footer; review the exact final text.
Verify the result
- Compare returned previous.id to the reviewed id, previous.approvalState=superseded, message.previousMessageId=previous.id and message.version=previous.version+1. Retain the new message.id.
- Read approvals.list and match the new pending id and exact subject/text. Read enrollments.messages.list with retained enrollmentId to verify the retained superseded and replacement versions.
Recover
After an uncertain edit, read the queue and enrollment history before writing again. The old id may already be superseded; repeating it will be refused. Do not edit the replacement automatically merely because the earlier response was lost. A reviewed correction can create another immutable version of the current still-pending message, preserving the full chain. No deletion or in-place rewrite of prior versions is declared.
Refused responses
Captured approvals.request_edit · HTTP 409
Action input
{
"id": "eng_msg_1",
"text": "x"
}
Actual local response
{
"error": {
"code": "state_conflict",
"message": "This message is not waiting for approval."
}
}
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.
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.