Workflow Intent API Reference
Workflow intent endpoints create, amend, complete, and read caller-declared workflow declarations.
Authorization: Bearer <project_api_key>
Content-Type: application/jsonWorkflow intent is available on every plan, Starter included. See Plan Availability.
Route summary
| Route | Purpose |
|---|---|
POST /v1/workflows | Declare a workflow. |
POST /v1/workflows/{workflow_id}/amend | Append an amendment to the workflow declaration. |
POST /v1/workflows/{workflow_id}/complete | Explicitly complete a workflow. |
GET /v1/workflows | List workflow declarations in the authenticated project. |
GET /v1/workflows/{workflow_id} | Fetch one workflow declaration with state and amendment history. |
POST /v1/workflows
Declares a workflow before it runs. The caller supplies workflow_id and an intent object. workflow_id is idempotent per project.
If the same workflow_id is declared again with the same canonical intent, Keel returns the existing declaration. If the same workflow_id is declared with a different canonical intent, Keel returns 409 with workflow_intent.idempotency_conflict.
Request
{
"workflow_id": "invoice-batch-2026-05-13",
"intent": {
"expected_calls": 10000,
"max_calls": 12000,
"expected_model": "gpt-5-mini",
"expected_input_tokens_per_call": 4000,
"expected_output_tokens_per_call": 500,
"max_duration_seconds": 86400
},
"budget_envelope_id": null
}| Field | Required? | Meaning |
|---|---|---|
workflow_id | Yes | Caller-supplied workflow handle, unique per project. Use [A-Za-z0-9_-], 1 to 255 characters. |
intent | Yes | Caller-declared workflow intent. |
intent.expected_calls | Conditional | Expected number of governed calls. Informational baseline for drift detection. Required if intent.max_calls is omitted. |
intent.max_calls | Conditional | Maximum governed calls allowed for this workflow. Enforcement ceiling. Required if intent.expected_calls is omitted. |
intent.expected_model | No | Model the caller expects the workflow to use. Used in projection methodology when supplied. |
intent.expected_input_tokens_per_call | No | Caller-declared input-token estimate per call. Used for cost projection. |
intent.expected_output_tokens_per_call | No | Caller-declared output-token estimate per call. Used for cost projection. |
intent.max_duration_seconds | No | Declared duration used to calculate expires_at. The existing 15-minute maintenance sweep expires at most 100 due workflows per run. Lifecycle expiry may lag the declared time; duration is advisory, not an exact dispatch deadline. Explicitly complete workflows you close. |
budget_envelope_id | No | Optional budget envelope in the same project, recorded on the signed declaration. It does not enforce dollars by itself; governed calls reserve against an envelope only when they send their own budget_envelope_id. Use null or omit when no envelope is recorded. |
At least one of intent.expected_calls or intent.max_calls must be set. When both are set, expected_calls must be less than or equal to max_calls.
Accepted response
Accepted and rejected declarations both return HTTP 200 with the same object shape. Fields that do not apply are null.
{
"id": "0e9d4c3a-5b6f-4a1e-9c2d-7f8e9a0b1c2d",
"workflow_id": "invoice-batch-2026-05-13",
"decision": "accepted",
"status": "active",
"version": 1,
"expected_calls": 10000,
"max_calls": 12000,
"cached_actual_calls": 0,
"projected_cost": {
"amount_micros": 22500000,
"currency": "USD",
"methodology": {
"basis": "caller_declared_workflow_x_point_pricing",
"provenance": "caller_declared_workflow",
"expected_calls": 10000,
"input_tokens_per_call_estimated": 4000,
"output_tokens_per_call_estimated": 500,
"pricing_table_id": "openai_v3.2_2026-04-15",
"tokenizer": "cl100k_base",
"quality": "authoritative"
}
},
"reason_code": null,
"decision_details": null,
"declared_by": {
"type": "api_key",
"id": "5a7c1e2f-3b4d-4c6e-8f9a-0b1c2d3e4f5a"
},
"declared_via": {
"sdk": "keel-python",
"sdk_version": "0.4.1"
},
"declaration_canonical_hash": "9f2c1e7a...hex-sha256",
"declaration_signature_b64": "base64-signature",
"declared_at": "2026-05-13T14:21:00Z",
"expires_at": "2026-05-14T14:21:00Z",
"completed_at": null,
"amendments": null
}| Field | Meaning |
|---|---|
id | Keel identifier for the declaration record. |
workflow_id | Caller-supplied workflow handle. |
decision | Declaration result: accepted or rejected. accepted means the workflow can run subject to policy and call limits. |
status | Lifecycle status: active, completed, expired, or rejected. New accepted declarations start as active. |
version | Declaration version. Starts at 1 and increments on each amendment. |
expected_calls | Effective expected call count. |
max_calls | Effective maximum call count. |
cached_actual_calls | Count of calls currently attributed to the workflow. |
projected_cost | Cost projection calculated from caller-declared intent, when Keel has enough pricing inputs. |
projected_cost.amount_micros | Projected cost in USD microdollars. |
projected_cost.currency | Currency for the projection. |
projected_cost.methodology | Recorded methodology for how the projection was calculated. |
projected_cost.methodology.basis | Projection basis. For workflow intent this records caller-declared workflow pricing. |
projected_cost.methodology.provenance | Provenance of the input values. Workflow intent uses caller_declared_workflow. |
projected_cost.methodology.expected_calls | Declared expected call count used in the projection. |
projected_cost.methodology.input_tokens_per_call_estimated | Declared input-token estimate per call used in the projection. |
projected_cost.methodology.output_tokens_per_call_estimated | Declared output-token estimate per call used in the projection. |
projected_cost.methodology.pricing_table_id | Pricing table used for the calculation. |
projected_cost.methodology.tokenizer | Tokenizer assumed for the estimate. |
projected_cost.methodology.quality | Methodology quality label for the projection inputs. |
reason_code | Rejection reason code; null when accepted. |
decision_details | Numeric inputs for a rejection; null when accepted. |
declared_by | Principal that declared the workflow. |
declared_by.type | Principal type: api_key or dashboard_session. |
declared_by.id | Identifier for the declaring principal. |
declared_via | Client-provided SDK metadata when present. These values are recorded as claims from the caller. |
declared_via.sdk | Claimed SDK name. |
declared_via.sdk_version | Claimed SDK version. |
declaration_canonical_hash | SHA-256 hex hash of the canonical declaration artifact. |
declaration_signature_b64 | Signature over the canonical declaration artifact. |
declared_at | Declaration timestamp. |
expires_at | Expiration timestamp when max_duration_seconds was declared; otherwise null. |
completed_at | Completion timestamp; null while active. |
amendments | Amendment history. null on declare and list responses; an array on amend and fetch responses. |
Rejected response
{
"id": "0e9d4c3a-5b6f-4a1e-9c2d-7f8e9a0b1c2d",
"workflow_id": "invoice-batch-2026-05-13",
"decision": "rejected",
"status": "rejected",
"version": 1,
"reason_code": "workflow_intent.declaration_exceeds_budget_cap",
"projected_cost": {
"amount_micros": 22500000,
"currency": "USD",
"methodology": {
"basis": "caller_declared_workflow_x_point_pricing",
"provenance": "caller_declared_workflow"
}
},
"decision_details": {
"current_monthly_spend_usd_micros": 810000000,
"monthly_cap_usd_micros": 825000000,
"projected_workflow_cost_usd_micros": 22500000,
"projected_monthly_spend_usd_micros": 832500000
}
}The rejected response has the same shape as the accepted response; the example shows only the fields that differ.
| Field | Meaning |
|---|---|
decision | rejected means the workflow was not admitted. |
status | rejected. |
reason_code | Stable reason code for the rejection. |
projected_cost | Projection that caused the rejection, when available. |
decision_details | Numeric inputs used for the decision. |
decision_details.current_monthly_spend_usd_micros | Current month-to-date spend before this declaration. |
decision_details.monthly_cap_usd_micros | Project monthly cap. |
decision_details.projected_workflow_cost_usd_micros | Projected workflow cost being evaluated. |
decision_details.projected_monthly_spend_usd_micros | Month-to-date spend plus the projected workflow cost. |
POST /v1/workflows/{workflow_id}/amend
Appends an amendment to a workflow declaration. The caller must send if_match_version so Keel can reject lost updates.
Request
{
"if_match_version": 1,
"new_expected_calls": 15000,
"new_max_calls": 20000,
"reason_provided": "ticket volume higher than expected today"
}| Field | Required? | Meaning |
|---|---|---|
if_match_version | Yes | Version the caller read before submitting the amendment. Keel rejects the amendment if the current version differs. |
new_expected_calls | No | Replacement expected call count. |
new_max_calls | No | Replacement maximum call count. |
reason_provided | No | Caller-provided reason for the amendment. |
At least one of new_expected_calls or new_max_calls must be supplied.
Response
Returns the updated declaration in the same shape as POST /v1/workflows, with amendments set to the full amendment history.
{
"id": "0e9d4c3a-5b6f-4a1e-9c2d-7f8e9a0b1c2d",
"workflow_id": "invoice-batch-2026-05-13",
"decision": "accepted",
"status": "active",
"version": 2,
"expected_calls": 15000,
"max_calls": 20000,
"cached_actual_calls": 9412,
"projected_cost": {
"amount_micros": 45000000,
"currency": "USD",
"methodology": {
"basis": "caller_declared_workflow_x_point_pricing",
"provenance": "caller_declared_workflow"
}
},
"declaration_canonical_hash": "9f2c1e7a...hex-sha256",
"declaration_signature_b64": "base64-signature",
"declared_at": "2026-05-13T14:21:00Z",
"expires_at": "2026-05-14T14:21:00Z",
"completed_at": null,
"amendments": [
{
"id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"workflow_declaration_id": "0e9d4c3a-5b6f-4a1e-9c2d-7f8e9a0b1c2d",
"applied_against_version": 1,
"previous_expected_calls": 10000,
"new_expected_calls": 15000,
"previous_max_calls": 12000,
"new_max_calls": 20000,
"reason_provided": "ticket volume higher than expected today",
"amendment_canonical_hash": "4b8e2d1f...hex-sha256",
"amendment_signature_b64": "base64-signature",
"created_at": "2026-05-13T16:05:00Z"
}
]
}| Field | Meaning |
|---|---|
version | New declaration version after applying the amendment. |
expected_calls | Effective expected call count after the amendment. |
max_calls | Effective maximum call count after the amendment. |
projected_cost | Recomputed projection after applying the amendment, when available. |
amendments | Ordered amendment history. |
amendments[].id | Amendment identifier. |
amendments[].workflow_declaration_id | Declaration the amendment belongs to. |
amendments[].applied_against_version | Version the amendment was applied against. |
amendments[].previous_expected_calls | Previous expected call count. |
amendments[].new_expected_calls | New expected call count. |
amendments[].previous_max_calls | Previous maximum call count. |
amendments[].new_max_calls | New maximum call count. |
amendments[].reason_provided | Caller-provided reason. |
amendments[].amendment_canonical_hash | SHA-256 hex hash of the canonical amendment artifact. |
amendments[].amendment_signature_b64 | Signature over the canonical amendment artifact. |
amendments[].created_at | Amendment creation timestamp. |
Other fields have the same meaning as in the POST /v1/workflows response.
Version conflict
Error details are merged into the error object.
{
"error": {
"code": "workflow_intent.amendment_version_conflict",
"message": "Workflow declaration version does not match if_match_version.",
"current_version": 3,
"if_match_version": 1
}
}| Field | Meaning |
|---|---|
error.code | Stable error or reason code. |
error.message | Human-readable summary. |
error.current_version | Current workflow declaration version. |
error.if_match_version | Version supplied by the caller. |
POST /v1/workflows/{workflow_id}/complete
Explicitly completes a workflow. Completion recounts attributed calls from permit records, reconciles the stored counter, and sets the status to completed. The route takes no request body.
Response
{
"workflow_id": "invoice-batch-2026-05-13",
"status": "completed",
"cached_actual_calls": 18433,
"authoritative_actual_calls": 18433,
"counter_reconciled": true,
"completed_at": "2026-05-13T18:30:00Z"
}| Field | Meaning |
|---|---|
workflow_id | Caller-supplied workflow handle. |
status | completed after successful completion. |
cached_actual_calls | Stored counter after reconciliation (equal to authoritative_actual_calls). |
authoritative_actual_calls | Count recomputed from permit records. |
counter_reconciled | true when the stored counter already matched the authoritative count; false when it diverged and was corrected. |
completed_at | Completion timestamp. |
GET /v1/workflows
Lists workflows in the authenticated project, newest first.
Query parameters
| Parameter | Meaning |
|---|---|
status | Optional lifecycle status filter: active, completed, expired, or rejected. |
created_at_from | Optional inclusive lower bound for creation time. |
created_at_to | Optional exclusive upper bound for creation time. |
limit | Optional page size, 1 to 200. Default 50. |
offset | Optional number of records to skip. Default 0. |
Response
{
"items": [
{
"id": "0e9d4c3a-5b6f-4a1e-9c2d-7f8e9a0b1c2d",
"workflow_id": "invoice-batch-2026-05-13",
"decision": "accepted",
"status": "active",
"version": 2,
"expected_calls": 15000,
"max_calls": 20000,
"cached_actual_calls": 9412,
"declared_at": "2026-05-13T14:21:00Z",
"expires_at": "2026-05-14T14:21:00Z",
"amendments": null
}
],
"limit": 50,
"offset": 0,
"total": 1
}| Field | Meaning |
|---|---|
items | Page of workflow declarations, each in the POST /v1/workflows response shape with amendments: null. |
limit | Page size applied. |
offset | Offset applied. |
total | Total matching declarations. |
GET /v1/workflows/{workflow_id}
Fetches one workflow declaration with its current state and amendment history. The response has the same shape as the amend response: the declaration fields plus amendments as an array (empty when there are none). Returns 404 with not_found when the workflow does not exist in the project.
There is no separate drift object. Compare cached_actual_calls with expected_calls and max_calls; drift transitions are recorded as workflow_intent.drift_detected chain events.
Workflow headers on governed requests
Subsequent governed requests are associated with a workflow by sending:
X-Keel-Workflow-Id: <workflow_id>See Header Convention for direct API and orchestrator guidance.