Commands

Endpoint: /api/events/commands/

Run commands on servers and read their results. A command runs in one of two ways:

  • Through a service token, under the token’s server and command access-control rules. This is the way for automation. See Service tokens.
  • Inside a work session, for a person signed in to the console or the CLI. Name the session in work_session; without one the request is refused with work_session_required. See Work sessions.

Personal API tokens cannot be issued command execution scopes or file-transfer scopes. Use a service token for automation and CI.

The examples on this page use a service token.

List commands

Retrieve a paginated list of commands.

Request

http
GET /api/events/commands/

Query parameters

ParameterTypeDescription
serverUUIDFilter by server ID
work_sessionUUIDFilter by work session ID
id__instringFilter by command IDs (comma-separated UUIDs, up to 100)
shellstringFilter by shell
requested_byintegerFilter by requesting user ID
requested_by_isnullbooleantrue for commands with no requesting user
service_tokenUUIDFilter by the service token that sent the command
applicationUUIDFilter by the application whose service token sent the command
api_tokenUUIDFilter by the personal API token that sent the command
request_channelstringHow the command was sent: web, cli, mcp, api_token, service_token, slack, unknown_client or unknown. Comma-separate several to match any
declared_agentstringCommands whose client declared this agent name
agent_commandbooleantrue for commands sent by an AI agent or a token, false for the rest
is_scheduledbooleantrue for commands scheduled in the future, false for the rest
verification_statusstringFilter by AI verification status: pending, awaiting_purpose, awaiting_approval, approved, rejected, skipped, monitor_executed or advisory
risk_levelstringFilter by risk level: INFO, LOW, MEDIUM, HIGH or CRITICAL. Case-insensitive; comma-separate several (HIGH,CRITICAL). An unknown value returns 400 with invalid_input
refusedbooleantrue for commands that were refused: rejected, or denied by a command access-control rule
denial_causestringRefused commands by group of rejection_cause: not_allowed (command_acl_denied, command_acl_unavailable), risk, approval or integrity_system
override_reasonstringUse session_less_service_token to find commands that ran after a rejecting verdict under the session-less service-token policy. Other values return 400 with invalid_choice
credential_deletedbooleantrue for token commands whose token has since been deleted
credential_exposure_acknowledgedboolean | nullCommands sent with credential_exposure_acknowledged
added_at__gtedatetimeCommands created at or after this time (ISO 8601)
added_at__ltdatetimeCommands created before this time (ISO 8601). With added_at__gte, a half-open range, so back-to-back ranges never count a command twice
searchstringSearch by server name, command line, or requester name
orderingstringSort field (default: -scheduled_at). Options: added_at, scheduled_at, delivered_at, acked_at, handled_at
pageintegerPage number
page_sizeintegerResults per page (default: 15, max: 100)

There is no filter on status; filter by verification_status, or read status from the results.

Response

json
{
  "count": 85,
  "current": 1,
  "next": 2,
  "previous": null,
  "last": 6,
  "results": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "shell": "system",
      "line": "df -h",
      "success": true,
      "exit_code": 0,
      "result": "Filesystem  Size  Used Avail Use% Mounted on\n/dev/xvda1  20G  8.5G  11G  46% /",
      "status": "completed",
      "cancellable": false,
      "response_delay": 0.125,
      "elapsed_time": 0.023,
      "added_at": "2026-10-20T10:30:44Z",
      "scheduled_at": "2026-10-20T10:30:44Z",
      "server": {
        "id": "7e3984de-49ab-4cc6-bcdf-21fbd35858b8",
        "name": "web-server-01",
        "os": "Ubuntu 22.04.4 LTS",
        "platform": "debian",
        "is_connected": true,
        "is_deleted": false
      },
      "username": "deploy",
      "groupname": "alpacon",
      "requested_by": null,
      "request_channel": "service_token",
      "work_session": null,
      "service_token": "3f1c2a9e-7b6d-4e1f-9a2b-5c8d7e6f4a31",
      "service_token_name": "ci-deploy",
      "application": "9d2e4b71-0c3a-4f5e-8b6d-1a2c3e4f5a6b",
      "application_name": "CI pipeline",
      "risk_level": "LOW",
      "verification_status": "approved",
      "override_reason": null,
      "purpose": "Check disk space before deploying"
    }
  ]
}

The list returns more fields than shown here, such as declared_agent and rejection_cause. override_reason is a read-only string | null in list and detail responses and in work-session timeline command items. Its only current non-null value is session_less_service_token: a command sent with a service token outside a work session ran even though AI verification rejected it. Such a command has verification_status: "monitor_executed". Other commands have override_reason: null, including commands that ran under signing monitor mode with the same verification status.

Example

bash
curl "https://your-workspace.us1.alpacon.io/api/events/commands/?ordering=-scheduled_at&page_size=20" \
  -H "Authorization: token=\"alpst-xxxxxxxxxxxxxxxxxx\""

Bulk status lookup

When tracking many commands at once (for example, a dashboard or automation polling for results), pass a comma-separated list of command IDs to id__in instead of issuing one GET /api/events/commands/{command_id}/ per command. Set page_size=100 as well: the default page size is 15, so without it a request for more than 15 IDs returns only the first page, with next set to 2.

bash
curl "https://your-workspace.us1.alpacon.io/api/events/commands/?id__in=550e8400-e29b-41d4-a716-446655440000,9b52a7c4-13de-48f6-a01b-7f3e5d982c44&page_size=100" \
  -H "Authorization: token=\"alpst-xxxxxxxxxxxxxxxxxx\""

Up to 100 IDs can be requested at once; exceeding this limit returns 400 with event_command_bulk_limit_exceeded, and any value that is not a valid UUID returns 400. An empty id__in value is ignored: the request behaves as if the parameter were omitted and returns the full accessible list. Results follow the ordering parameter (default: -scheduled_at), not the order of the IDs in the request, so match each result to your list by its id field. IDs for commands you cannot access or that no longer exist are silently excluded; compare the response count with the number of unique IDs you requested to detect exclusions.

Command status

status is a string that Alpacon works out each time you read the command:

StatusMeaning
scheduledWaiting for its scheduled_at time
queuedDue, and waiting to be delivered, for example until the server reconnects or a run_after command finishes
verifyingAlpacon AI is judging it
awaiting_purposeWaiting for the requester to say what the command is for
awaiting_approvalHeld for an approver
deliveredSent to the server
runningThe server started it
stuckDelivered or started, with no progress for a long time
completedFinished. Read success and exit_code for the outcome
rejectedRefused by a verdict before it ran: the AI, an approver, a workspace rule, or the token’s command rules. rejection_cause says which, for example risk_denied, approval_rejected, approval_policy_denied or command_acl_denied
deniedStopped before it ran without a verdict: the token’s access-control rules refused it when it was sent, the agent session’s approved account doesn’t cover it, or its work session ended first. denial_code says why; the list response also has denied: true. Not the same as rejected. A token command its command rules refuse is rejected on Alpacon Cloud and denied on a self-hosted deployment; see Refused by the token’s command rules
errorThe command’s state couldn’t be determined

On the detail route, read rejection_cause, rejection_gate and rejected_at for a rejected command, and denial_code for a denied one. For a completed command, read success first, then exit_code, result, elapsed_time and handled_at; when success is false, error_phase can say where it failed. A stuck command that times out ends as completed with success: false, exit_code: null and a timeout message in result.

cancellable is a separate boolean: true while the command can still be cancelled.

Each command also records its progress in timestamps:

FieldDescription
added_atCommand created
scheduled_atScheduled run time
delivered_atSent to the server
acked_atThe server acknowledged it and started running it
handled_atFinished

Rejection causes

rejection_cause uses the values below. rejection_gate is null for the two signature causes and for account_bound_to_other_person.

rejection_causerejection_gateMeaning
command_acl_deniedcommand_aclThe command is not pre-approved by the token’s command rules
command_acl_unavailablecommand_aclThe command pre-approval decision is unavailable
risk_deniedriskRisk control refused the command
risk_unavailableriskThe risk judgment is unavailable
approval_policy_deniedapprovalThe approval policy refused it or no eligible approver other than the requester is available
approval_rejectedapprovalAn approver rejected the request
approval_expiredapprovalThe pending approval expired
approval_cancelledapprovalThe requester cancelled the approval request
work_session_terminatedwork_sessionThe work session ended while the command was held
account_bound_to_other_personnullThe run-as account became linked to another person before delivery
unsigned_filenullThe file command has no signature
signature_invalidnullThe agent reported an invalid signature
use_file_executionapprovalAn access rule requires the script to run through file execution

Denial codes

denial_code is null unless the command was denied. A client can branch on these fixed values:

denial_codeMeaning
command_acl_no_matchNo command rule on the token allows the line
server_acl_deniedThe token’s server rules leave out the server
acl_not_configuredThe token has no server or command rules
command_rejected_by_parserThe line uses shell syntax no command rule can allow
work_session_terminatedThe work session ended before the command was delivered
account_bound_to_other_personThe run-as account became linked to another person before delivery
work_session_account_not_approvedThe agent’s work session doesn’t approve the run-as account
account_not_resolvedThe command names no account or group to run as
account_group_not_permittedThe requester may not run as that group
deniedDenied for another reason

For a work session you can’t view, or one that no longer exists, work_session_terminated reads as denied.

Error phases

error_phase says where a command went wrong when success is false or the command is stuck. It is null for a command that succeeded, and for a command that ran and exited non-zero for its own reasons, such as exit 1.

error_phaseWhen
agent_disconnectedstuck: the server never acknowledged the command, likely because it disconnected
agent_timeoutstuck: the server acknowledged the command but hasn’t reported a result
remote_command_exceeded_timeoutThe command ran past its time limit on the server (exit code 124)
remote_command_not_executableThe command was found but couldn’t be run (exit code 126)
remote_command_not_foundThe command wasn’t found on the server (exit code 127)
remote_command_killedThe command was killed by a signal
dependency_cancelledThe command never reached the server because a run_after command it waited on failed or was refused
file_hash_mismatchScript: the file changed after it was checked, so the server refused to run it. Send it again so the current file is checked
file_payload_invalidScript: the server couldn’t read the request. Send it again; if it keeps failing, upgrade the agent
file_open_failedScript: the server couldn’t open the file. Check the path exists and is a readable regular file
file_exec_unsupportedScript: this server’s platform can’t run scripts. Send a command line instead
file_too_largeScript: the file is larger than the server will run

alpacon exec prints the phase on stderr, with a description for most phases and a next step for the script phases. The MCP command tools add a similar next step as error_phase_hint.

Sudo approval

A command that runs sudo can wait for an approver. Two detail fields follow that wait:

  • sudo_grant_status is null when the command has no sudo request. Otherwise it is pending_mfa (waiting for the requester’s MFA check), pending_approval (waiting for an approver), authorized (approved), used (the approved sudo ran), rejected or expired.
  • sudo_approval_request_id names the approval request while sudo_grant_status is pending_approval, and is null otherwise. It turns null both when the request is approved and when it is rejected, so read sudo_grant_status to tell the two apart.

alpacon exec waits on these fields when a sudo command needs approval. Both are detail-only; poll Get command while you wait.

Script fields

approved_file_execution is set when a standing approval let a script run without a new approval, and stays set after that approval is revoked or expires. With the approved_file_execution:read permission it carries id, expires_at, approved_by (id, username), approval_request (id), either of which can be null, revoked_at and revoke_reason (manual, approver_removed, approver_deactivated, expired, or empty). Without it, only id.

file_execution_suggestion is set while a command line that runs a script file is held (awaiting_approval or awaiting_purpose), and the line could be sent again as a script request. It carries path, args, interpreter (an absolute path, or null), interpreter_name (a bare name such as bash, or null; supply the absolute path yourself before sending) and repeatedly_approved, which is true when people approved this exact line often enough recently to make it a candidate for a standing approval. To send it again, add the script’s content and copy the server, username and groupname from the held command. It is null when masking changed the line, or when the script request would be refused anyway.

Error responses

StatusError codeDescription
400api_insufficient_dataNo target server, or no line or file
400does_not_existOn server: the server is outside the service token’s server access rules, or does not exist
400server_not_connectedThe single server is not connected
400server_not_commissionedThe single server is not commissioned
400command_inline_credentialline contains an inline credential. Move it to env, or set credential_exposure_acknowledged: true to proceed
400work_session_requiredA person signed in to the console or the CLI sent a command without work_session
400work_session_not_active, work_session_expired, work_session_scope_not_allowed, work_session_server_not_allowedThe work session isn’t active, has ended, doesn’t include commands, or doesn’t include this server
400event_command_bulk_limit_exceededMore than 100 command IDs passed to id__in
400event_command_run_after_too_manyMore than 100 commands in run_after
400file_exec_content_too_large, file_exec_empty_content, file_exec_invalid_path, file_exec_line_not_allowed, file_exec_data_not_allowed, file_exec_env_not_allowedThe file request is invalid: the script is over 64 KB or empty, a path isn’t absolute, or it carries line, data or env
400event_command_already_sentCancel: the command was already delivered to the server
400event_command_not_cancellableCancel: a firewall command can’t be cancelled
401auth_mfa_requiredThe command runs as an account that isn’t yours, and needs a recent MFA check. The body also carries "source": "command"
403api_token_scope_missingThe token lacks command:create (or command:read to read). missing names the scope
403api_token_acl_not_allowedOn a self-hosted deployment, the token’s command rules don’t allow the line. Retrying does not help—widen the token’s rules
403event_command_not_requesterCancel: only the requester or a Superuser can cancel a firewall command
404api_not_foundCommand not found

Every error body is coded; see Error messages.

Refused by the token’s command rules

A command the token’s command rules don’t allow never runs. How it is refused depends on the deployment:

  • Alpacon Cloud: the request is accepted with 201, and the command is then refused before it runs: its status becomes rejected with rejection_cause command_acl_denied. If the rules couldn’t be checked at that moment, rejection_cause is command_acl_unavailable instead, and sending the command again can succeed.
  • Self-hosted: the request is refused with 403 and api_token_acl_not_allowed, and the command is recorded with status denied. A self-hosted deployment whose command rules have been moved to Alpacon AI behaves like Alpacon Cloud.

For command_acl_denied or api_token_acl_not_allowed, retrying does not help—widen the rule. See Command patterns and sudo.

Last updated: