Appearance
Agents and AI
Actuator is built for both humans and AI agents to operate the same surface. The REST API, the audit log, the evaluator, and the MCP server are peers — agents are not second-class consumers. This page consolidates the agent-relevant material into one orientation.
A quick map of what's below:
- What surface do agents have? Scoped tokens with five capability levels, six action verbs, and a delegation chain that tops out at the human who minted the token — see Token model.
- How do agents reason about a change before making it? A counterfactual preview endpoint that evaluates a hypothetical ruleset against a list of contexts — see Counterfactual preview.
- How do agents propose changes safely? A two-phase propose / apply flow with version-drift detection — see Propose / apply.
- How do agents connect? Either via REST or the first-party MCP server — see MCP server.
Token model
Every API token declares a scope along several dimensions:
| Dimension | Values |
|---|---|
| Environments | Specific env(s), all envs in a project, or all envs in the org |
| Resources | Specific keys, key prefixes (payments.*), or all |
| Actions | read, propose, write, toggle, promote, delete |
| Capability level | observer, proposer, operator, maintainer, admin |
| Rate limit | Requests per minute per token (default 60) |
| TTL | Auto-expires (1 hour to 90 days; renewable) |
| Approval required | Whether mutations need human approval before taking effect |
| IP allowlist | Optional CIDR range restriction |
The five capability levels are pre-baked scope templates that reduce misconfiguration:
- Observer — read-only. Audit queries, flag/config reads, evaluation tracing. The default for agents whose job is to investigate, summarize, or report.
- Proposer — read + create proposals. Cannot mutate directly; every change goes through the approval workflow. The safe default when an agent acts on behalf of a user with limited authority, or when an operator wants review on every agent action.
- Operator — toggle existing flags, adjust rollout percentages, edit existing rules. Cannot create or delete flags, cannot change approval settings. The on-call assistant profile.
- Maintainer — full CRUD on flags and configs. Cannot change RBAC, rotate tokens, or modify approval rules. Suitable for agents that manage flag lifecycle (creation, cleanup, scheduled changes).
- Admin — everything. Intended for humans, not agents. Granting Admin to an agent is supported but requires an explicit override and generates a prominent warning.
A safe default agent token: read + propose on production, write + toggle on staging, 7-day TTL, 60 req/min.
Delegation
A user can mint short-lived tokens scoped at-or-below their own permissions and hand them to agents. Token minting is itself an audited action. Delegated tokens inherit a ceiling from the delegator — agents cannot escalate beyond their human's permissions, and revoking the human cascades-revoke every token they delegated. This is the default pattern for giving an engineer's personal agent temporary elevated access without a permanent configuration change.
The audit row for every agent-originated mutation carries the full chain: actor_token_id → delegator_user_id → (if approval-gated) approver_user_id.
Counterfactual preview
POST /api/v1/envs/:envId/evaluate/preview evaluates a hypothetical ruleset against a list of contexts and returns both the live and preview resolution side-by-side, so an agent can reason about blast radius before committing a change.
http
POST /api/v1/envs/{envId}/evaluate/preview
Content-Type: application/json
{
"spotCheck": [
{ "userId": "u_42", "plan": "enterprise" },
{ "userId": "u_99", "plan": "free" }
],
"ruleset": {
"flags": [
{
"key": "ui.theme",
"type": "string",
"rules": [],
"defaultValue": "midnight"
}
]
},
"verboseReason": false
}The response carries the env's current liveVersion plus, for each context, the live resolution and the preview resolution:
json
{
"environmentId": "9f7a32b5-1234-4abc-9def-...",
"liveVersion": 142,
"spotCheck": [
{
"context": { "userId": "u_42", "plan": "enterprise" },
"live": { "ui.theme": { "value": "classic", "defaultValue": "classic", "reason": { "kind": "default" } } },
"preview": { "ui.theme": { "value": "midnight", "defaultValue": "midnight", "reason": { "kind": "default" } } }
},
{
"context": { "userId": "u_99", "plan": "free" },
"live": { "ui.theme": { "value": "classic", "defaultValue": "classic", "reason": { "kind": "default" } } },
"preview": { "ui.theme": { "value": "midnight", "defaultValue": "midnight", "reason": { "kind": "default" } } }
}
]
}Notes:
- The transient ruleset is subject to the same save-time validation as a write would be (cycle detection, regex compilation, JSON Schema, type compatibility) — invalid rulesets return 400 with the same errors
POST /flagswould have produced. - The preview is not stored anywhere server-side and does not bump any version.
- A request is capped at 50 spot-check contexts. The endpoint is for spot-checking specific personas (the CEO, your test fixtures, named accounts), not for scanning a user base.
asOfandrulesetare mutually exclusive; preview is always against the supplied ruleset.
The dashboard's "Preview impact" panel on the flag detail page is a thin client over this endpoint.
Propose / apply
The propose / apply flow is the durable, auditable equivalent of "agent suggests, human approves." It builds on the preview endpoint above: the agent captures the blast radius at propose time, and the applier verifies the live ruleset hasn't drifted before letting the change land.
Lifecycle
pending (TTL: 1h default, 24h max)
│
├──► applied (apply handler committed the diff)
├──► cancelled (explicit reject by proposer or operator)
└──► expired (background sweeper after expiresAt)A proposal is single-use: any second apply or cancel against a non-pending row returns 410 proposal_gone, and an apply against a cancelled row returns 410 too.
Propose
http
POST /api/v1/proposals
Content-Type: application/json
{
"envId": "9e1f…",
"kind": "set_default_value_flag",
"resourceKey": "ui.theme",
"diff": { "defaultValue": "midnight" },
"spotCheck": [
{ "userId": "u_42", "plan": "enterprise" }
],
"reason": "switch default theme; 0 of 1 sampled contexts flip",
"expiresInSeconds": 3600
}spotCheck is capped at 50 entries. expiresInSeconds is optional; default 3600 (1h), max 86400 (24h). resourceKey identifies the target primitive — the resource type is implied by kind and validated server-side.
Returns 201 Created with Location: /api/v1/proposals/{id} and a Proposal body that carries:
liveVersion— the env's version at propose time. Compared at apply time for drift detection.blastRadius— the materialized output of running/evaluate/previewagainstspotCheckwith the proposed ruleset. Each entry has{context, live, preview}envelopes.expiresAt— absolute timestamp; the sweeper uses this.status: "pending", plus the proposer attribution (proposerTokenIdorproposerUserId).
Error responses:
409 in_usefordelete_segmentof a segment referenced by another flag/config;fieldslists referrers as<resourceType>.<resourceKey>filtered by the caller's scope.503 ruleset_not_cachedif the env has no live blob yet (a freshly created env hits this until its first write).
Kinds (the verb vocabulary)
kind selects the mutation; each kind names a concrete verb the apply path switches on.
| Resource | Kinds |
|---|---|
| Flag | set_default_value_flag, set_rules_flag, kill_flag, delete_flag |
| Config | set_default_value_config, set_rules_config, delete_config |
| Segment | set_rules_segment, delete_segment |
diff is kind-specific:
set_default_value_*→{ "defaultValue": <typed> }set_rules_*→{ "rules": [<rule>, ...] }(segments also acceptallowList?,denyList?)kill_flag→{}(boolean flags only; the verb is "set defaultValue=false, rules=[]")delete_*→{}
Get
http
GET /api/v1/proposals/{id}Returns the same Proposal body propose returns, useful when the agent host's context window has been compacted and you need to re-read the blast radius before applying. Cross-tenant access collapses to 404.
Apply
http
POST /api/v1/proposals/{id}/applyInside a single transaction, the apply handler:
- Takes a row-level lock on the proposal, asserting
status = 'pending'. - Reads the env's current version.
- Compares it to the captured
liveVersion.- Mismatch: returns
409 version_driftwith body{liveVersion, proposedVersion}. The proposal stays terminal-eligible (stillpendinguntil the sweeper expires it); the agent is expected to re-propose against the new live state, which captures a freshblastRadius. - Match: executes the diff via the canonical write path (audit + cache + version bump). The audit row carries
reason="proposal:<id>"; the proposal back-links viaappliedAuditId.
- Mismatch: returns
- Marks the proposal
appliedwithresolvedAt,resolverTokenIdorresolverUserId,appliedAuditId, andappliedVersionset to the new env version.
Returns 200 OK with:
json
{
"proposalId": "...",
"status": "applied",
"appliedVersion": 143,
"appliedAuditId": "...",
"resolvedAt": "2026-04-29T14:33:00Z"
}Error responses:
409 version_drift— env was written between propose and apply.409 in_use—delete_segmentapply finds new referrers that landed between propose and apply.410 proposal_gone— proposal is already applied or past itsexpiresAt.
The version-drift check is the load-bearing safety property: two concurrent applies can't both succeed — the second sees the new env version and the comparison fails.
The propose action gates POST /proposals; the apply action gate matches the underlying CRUD verb (write for set/kill kinds, delete for delete kinds), so a Proposer-level token can stage any change but only an Operator+ token can apply.
Cancel
http
POST /api/v1/proposals/{id}/cancel
Content-Type: application/json
{ "note": "human reviewed; not shipping this change" }Body is optional — {} cancels without a note. Reachable by:
- the original proposer (regardless of action scope), so an agent can withdraw its own pending proposal; or
- anyone holding the action the apply would have required (so an Operator+ token can reject any pending row in scope).
Returns the canceled Proposal row with status='cancelled', resolvedAt, the resolver attribution, and resolverNote (also copied to the proposal's reason if set).
Error responses:
410 proposal_gone— proposal is already applied, cancelled, or past itsexpiresAt.
Expire
A background sweeper inside the server flips status='expired' for any pending row past its expiresAt. Expired proposals can't be applied; the agent re-proposes if the human takes too long.
Audit chain
Every mutation — agent or human, write or apply — is recorded with the full accountability chain:
| Column | Meaning |
|---|---|
actor_type | user | api_token | agent_token | system |
actor_id | Token id (or user id, for human actions) |
delegator_user_id | The human who minted the token, if delegated |
approver_user_id | The human who approved the apply, if gated |
resource_type, resource_key, resource_id | What changed |
previous_value, new_value, diff | The change payload |
reason | Free-text — e.g., "incident #4821 mitigation" or "proposal:4f2a" |
The dashboard's audit page filters by actor type, actor, time range, and resource. Every column above is queryable via GET /api/v1/orgs/:slug/audit.
Proposal lifecycle is also audited
Every proposal transition writes its own audit row with resourceType=proposal and one of the following actions:
proposal.created— whenPOST /proposalssucceeds.proposal.applied— whenPOST /proposals/{id}/applycommits.proposal.cancelled— whenPOST /proposals/{id}/cancelflips it.proposal.expired— when the sweeper flips a TTL'd row.
This timeline is independent of the data-change rows. When a proposal applies, the underlying primitive's audit row carries reason="proposal:<id>" and the proposal-side row's appliedAuditId back-links — so two queries reconstruct the full story:
audit?resourceType=proposal&resourceId=<id>— the proposal's lifecycle (created → applied / cancelled / expired).audit?resourceType=flag&resourceKey=<key>(orconfig/segment) — the data-change timeline; rows produced by an apply are tagged withreason="proposal:<id>".
MCP server
A first-party Model Context Protocol server, distributed as the @actuator/mcp package (npx @actuator/mcp). The shipped tool surface mirrors the REST endpoints — agents that prefer raw HTTP can call those directly with no loss of capability.
| Category | Tools |
|---|---|
| Navigation | list_organizations, list_projects, list_environments, list_flags, list_configs, list_segments |
| Describe | describe_flag, describe_config, describe_segment |
| Evaluation | evaluate_for_context, lookup_flags_for_target, get_ruleset_version |
| Audit | audit_query, describe_audit_event |
| Propose (one verb per kind) | propose_set_default_value, propose_set_default_value_config, propose_set_rules_flag, propose_set_rules_config, propose_set_rules_segment, propose_kill_flag, propose_delete_flag, propose_delete_config, propose_delete_segment |
| Apply / cancel / inspect | apply_proposal, cancel_proposal, describe_proposal |
| Direct create | create_flag, create_config, create_segment |
| Metadata edits | update_flag_metadata, update_config_metadata, update_segment_metadata |
| Append-rule helpers | append_flag_rule, append_config_rule, append_segment_rule |
The propose tools are kind-specific so each one carries a typed schema for its diff — the agent host can validate the call locally before sending. Every propose tool returns a proposalId and the materialized blast radius; the canonical pattern is "propose → present blast radius to the human → call apply_proposal(proposalId) on approval."
create_* and the metadata/rule-append helpers exist alongside the propose surface for low-risk operations where round-tripping a human approval would be friction with no safety win — they call the underlying POST /flags / PATCH /flags/{key} endpoints directly under the calling token's write action gate. A token without write falls through to using the propose tools.
Every mutation tool respects the calling token's capability level and the env's approval-gate configuration. An Observer-level token calling a write tool receives a structured permission error pointing it at the matching propose_* tool instead.
See also
- Concepts → Evaluation — the evaluation model, time-travel, history.
- API reference: Evaluation — wire shape for evaluate, preview, history, validate.
- API reference: Proposals — full propose / apply / cancel reference.
- API reference: Audit — querying the audit log.
- API reference: API keys — minting tokens.