Action Controls
Let a reviewer request a bounded secondary operation without resolving the Action.
Action Controls sit beside an Action's main response. They let a reviewer request an operation such as retrying a job or rolling back a deployment while the Action stays open. A link control opens supporting material without creating a Control Request.
Controls are available on paid plans when the feature is enabled for the workspace. A plan without the feature returns 403 ACTION_CONTROLS_PLAN_REQUIRED. A workspace where the staged feature is not enabled returns CONTROLS_DISABLED.
Add controls in two minutes
Use the SDK helpers to keep control objects small and explicit:
import os
from actionbox import Actionbox, control, link
with Actionbox(os.environ["ACTIONBOX_API_KEY"]) as client:
action = client.create(
title="Import stalled",
description="72,831 of 184,221 records processed.",
callback_url="https://worker.example.com/actionbox",
controls=[
control("retry", "Retry"),
control("stop", "Stop", destructive=True),
link("logs", "Open logs", "https://logs.example.com/imports/4821"),
],
)The reviewer sees the controls beside the main decision:
IMPORT STALLED
72,831 / 184,221 records processed
[ Retry ] [ Stop — hold to confirm ]
Open logs ↗Retry and Stop create Control Requests. Open logs is only a link. None of them answers or closes the Action.
What happens after a reviewer taps Retry
Reviewer taps Retry
↓
Action version and fingerprint are validated
↓
A durable Control Request is recorded
↓
ActionBox sends a signed action.control_requested webhook
↓
The Source allowlists "retry" and performs the operation
↓
The Source reports running, succeeded, or failed
↓
The Action history shows delivery and result separatelyActionBox records and delivers the request. It does not run your infrastructure command. The Source integration must verify the signed callback, allowlist the control key, perform the operation, and report the result.
Control schema
An Action accepts up to 12 controls, with no more than 3 mutating controls. Keys must be unique.
| Field | Type | Required | Constraint |
|---|---|---|---|
key | string | Yes | 1 to 64 characters. Start with a lowercase letter, then use lowercase letters, numbers, _, or -. |
label | string | Yes | 1 to 80 characters. |
kind | string | Yes | callback, link, or actionbox. |
url | string | For link | HTTP or HTTPS URL, up to 2,048 characters. Rejected for other kinds. |
destructive | boolean | No | Defaults to false. Link controls cannot be destructive. |
expires_at | string | No | A future timestamp after which the control is unavailable. |
callback controls use the Action's callback_url, so the Action must define one. actionbox controls are reserved for Watch operations and accept only pause_watch, resume_watch, or skip_current_occurrence as the key.
{
"title": "Deployment failed",
"description": "The health check timed out.",
"callback_url": "https://ci.example.com/actionbox",
"controls": [
{ "key": "retry", "label": "Retry deployment", "kind": "callback" },
{
"key": "rollback",
"label": "Roll back",
"kind": "callback",
"destructive": true
},
{
"key": "logs",
"label": "Open logs",
"kind": "link",
"url": "https://ci.example.com/runs/4821"
}
]
}Invoke a mutating control
The web and mobile clients call:
POST /v1/actions/{action_id}/controls/{control_key}/invoke
Idempotency-Key: retry-deploy-4821
Content-Type: application/json
{
"action_version": 3,
"fingerprint": "sha256:<64 lowercase hex characters>"
}The version and fingerprint bind the request to what the reviewer saw. The API rejects stale bindings and permits one active mutating request per Action. Reusing the same idempotency key for the same request returns the original Control Request.
Destructive controls require a press and hold in the supported review clients. Treat that gesture as confirmation, not authorization. The server still checks the signed-in user's workspace role and current Action binding.
Receive and report the operation
A callback control produces a signed action.control_requested webhook. Verify it with the Source webhook secret used for other Action callbacks. The payload identifies the Control Request, Action, exact Action version and fingerprint, control key, requesting user, and Source.
The owning Source reports progress to:
POST /v1/source/control-requests/{control_request_id}/result
Authorization: Bearer $ACTIONBOX_SOURCE_KEY
Content-Type: application/json
{
"status": "succeeded",
"message": "Deployment recovered"
}| Field | Type | Required | Constraint |
|---|---|---|---|
status | string | Yes | running, succeeded, or failed. |
message | string | No | Up to 500 characters. |
reason_code | string | No | 1 to 80 uppercase letters, numbers, or underscores; must start with a letter. |
Reporting a control result does not resolve the parent Action. Delivery state and operation state remain separate in the audit trail. GET /v1/actions/{action_id}/control-requests returns the latest 100 requests visible to the signed-in workspace.
Allowlist every operation
Never execute a command assembled from a label, URL, or untrusted payload field. Map each known control key to a bounded operation in your Source integration and reject unknown keys.
The CLI accepts controls through --controls-json JSON|@FILE. The Python and TypeScript SDKs accept the same wire objects. MCP ask_human accepts controls and callback_url; MCP update_action can replace controls on an open Action.
Next: Webhooks and callbacks, Webhook operations, or Decision context.