Run a flow and read its model calls
Send a request through the flow bound to an operation code, read the operations — the model calls of the flows — with every attempt behind them, and list the holds of those calls.
Two things of this page share a name. A request to a flow names an operation code, and POST /v1/operations/execute runs the flow bound to that code. An operation is also the record of one model call of a flow, and two reads of this page list such records and show one with every attempt behind it. The last operation of the page lists the holds of those calls. The console shows the same records on Operations, on its Operations tab — Operations, authorizations and the ledger walks through it.
An execution and an operation
POST /v1/operations/executeanswers with an execution: one walk of the flow, named byexecution_id. An execution is read again withGET /v1/executions/{id}/statusandGET /v1/executions/{id}.An operation is one model call, named by
operation_id. One is opened for each model call a flow makes.An execution id is not an operation id:
GET /v1/operations/{id}answers404with the codeoperation_not_foundfor one.
The record of a model call
An operation is opened when an AI Model node is about to call a model, after the money for the call is held. It starts pending and ends in one of three statuses.
| Status | What it means |
|---|---|
completed | The model answered |
failed | The call ended without an answer of the model |
uncertain | It is not known how the call ended: the provider did not answer in time, or it took the call to run in the background and could not be asked about it |
The hold of an uncertain call is not given back at once: the provider may have been paid, so the hold stays until it expires. Such a call is not repeated by Slen either. When the answer of a call is lost, the operation.uncertain webhook event announces it. A call the provider runs in the background announces how it ended too: operation.completed when the provider answered, operation.failed when the call failed, and operation.uncertain when the provider could no longer be asked about it. To find the calls whose outcome is unknown without a webhook, list the operations by their status, as the example below does.
An attempt is one call to one provider connection. When the pool of the model moves on to another connection after a failure, the operation gets the next attempt, numbered in order. An attempt says which connection and model it used and what came of it. Its cost stays null until the call is settled.
The list holds the operations of the project of the key, the newest first. It leaves the attempts out:
attemptsis an empty list there. Read one operation to see them.An operation keeps no prompt and no answer of the model.
An attempt names its connection and its pool:
connection_nameandpool_name, withproviderandconnection_deletedbeside them. A name isnullwhen the pool has been deleted since —pool_idstays — or when the connection or the pool is out of the reach of the key. A deleted connection keeps its name.finish_reasonis why the provider stopped, as it reported it.flow_executionnames the execution and the node whose model call the operation is:execution_id,flow_idandnode_key. It isnullwhen the execution is out of the reach of the key, and only one operation is read with it, never the list.
Search and numbered pages
q searches the list. Digits that are the ID of an operation of the project find exactly that operation, however old it is: the window of from and to does not apply to it, while status and operation_code still do. Any other text finds the operations whose external_user_id is that text or starts with it, inside the window; % and _ are plain characters there. A q of more than 190 characters is answered with 422. The value is part of the address, so an external_user_id that is an e-mail address ends up in browser histories and access logs.
The list is read with a cursor, as every list is — A list, page by page — or by the number of a page. Send page, and meta carries page, limit, total and anchor. total is counted up to 10,000; past that it stays 10,000 and total_capped is true. Send the anchor of the first page back with every other page: only the operations at or below it are counted, so an operation opened in the meantime does not move the rows from one page to the next. page together with cursor, and anchor without page, are answered with 422.
The holds of the calls
GET /v1/authorizations lists the holds of the project, newest first: one for each model call, with what the call was expected to cost, what was held and what was captured. It takes the scope read and reads a window of at most seven days, as the list of operations does, with a cursor or by the number of a page. operation_code keeps the holds of one operation code. How budget reservations work says what each status of a hold means, and the console shows the same holds on the Authorizations tab of Operations.
Operations
Run the flow bound to an operation server-side
POST /v1/operations/execute
- Scope:
gateway:write - Idempotency-Key required
- Available in the SDK for PHP and Node.js
The caller names what it wants done, never a model or a pool. Answers 202 when the flow parked at a queue or approval node — waiting_on says on what — and also when execution_mode: async handed the run to a queue worker, where the status is queued and nothing has walked yet. Both carry retry_after_seconds. Idempotent.
The Idempotency-Key belongs to the execution it opened, and no copy of the answer is kept beside it: a repeated key with the same body is answered from that execution as it is now (replayed: true) and runs nothing. 202 with retry_after_seconds while it has not finished — queued, waiting, or running when the retry met a walk still under way (a synchronous run may outlast the caller's timeout) — and 200 with the outcome once it has: output of a completed run, error_code and error_message of one that was refused or failed, even when the first answer was an error envelope — error repeats the two as {code, message}, on a replay exactly as on the first answer. The same key with a different body is 409 idempotency_conflict.
A flow that refuses is not an HTTP error: a reject node answers 200 with status: rejected. A walk that throws is one: the execution is closed — failed, rejected when the gateway refused to spend (policy_denied, customer_blocked, budget_exceeded, insufficient_credits, an overrun), uncertain when the provider's answer was lost — with the same code in its error_code, and the answer is the error envelope with the status of what threw. The code of the envelope and the code of the row are one and the same, also for what is wrong with the setup rather than refused by a policy: customer_not_found (404), model_not_allowed (403), cost_calculation_failed (422) and ledger_unavailable (503) close the execution failed with that code.
A synchronous run that outlives the running ceiling (15 minutes by default) is closed failed with execution_lost while its walk is still under way. Whatever that walk goes on to compute is not the outcome of the execution, and the call is answered with the execution as its row has it — 200, failed, execution_lost, no output, replayed: false — exactly what a repeated key is told afterwards. A model call the walk did make is paid for, and cost.actual arrives.
A model node routes a refusal to spend into its deny port when something is connected to it — policy_denied, customer_blocked, budget_exceeded, insufficient_credits, overrun_stopped — and then the flow decides what the caller hears. The setup errors above are never routed.
403 customer_blocked — the context.user_id names a customer that is blocked (POST /v1/customers/{externalUserId}/block). Nothing is created: no execution, no hold. A repeated key of an execution opened before the block is still answered by that execution. A run already under way — queued, or parked at an approval — meets the block at its next model node that takes credits: the hold is refused, and the execution closes rejected with customer_blocked (a model node with something on its deny port routes it there, like a policy refusal). A run whose operation has no price in credits takes no hold and finishes.
404 flow_not_found — no flow is bound to the operation, the flow was never published, or it is archived. Nothing is created for such a call — no execution, no hold — so the same key runs once the flow is un-archived. An execution parked before its flow was archived still finishes, and the key of an execution that exists is answered by it whatever became of the flow or its binding since: the 404 is for a key that opened nothing.
Parameters
| Parameter | Description |
|---|---|
Idempotency-Key requiredheader | Replay guard for a financial write. The same key returns the original result with the original status and never charges twice; a different body under a used key answers
|
Request body
{
"operation": "generate_email",
"input": {},
"context": {
"user_id": "74438200973594624",
"credit_type": "default"
},
"execution_mode": "sync"
}Responses
The finished run
{
"data": {
"execution_id": "74438200973594624",
"status": "running",
"operation": "generate_email",
"output": {},
"error_code": "string",
"error_message": "string",
"cost": {
"estimated": "0.02700000",
"actual": "0.02700000"
},
"cache": {
"hit": true,
"cached_at": "2026-10-02T09:30:00Z"
},
"waiting_on": {
"node_key": "string",
"type": "queue_node",
"resume_at": "2026-10-02T09:30:00Z"
},
"trace_url": "string",
"retry_after_seconds": 1,
"error": {
"code": "string",
"message": "string"
},
"replayed": true
}
}The run has not finished: it parked, was queued, or — on a repeated key — is still walking. Come back after retry_after_seconds.
{
"data": {
"execution_id": "74438200973594624",
"status": "running",
"operation": "generate_email",
"output": {},
"error_code": "string",
"error_message": "string",
"cost": {
"estimated": "0.02700000",
"actual": "0.02700000"
},
"cache": {
"hit": true,
"cached_at": "2026-10-02T09:30:00Z"
},
"waiting_on": {
"node_key": "string",
"type": "queue_node",
"resume_at": "2026-10-02T09:30:00Z"
},
"trace_url": "string",
"retry_after_seconds": 1,
"error": {
"code": "string",
"message": "string"
},
"replayed": true
}
}Errors
| Status | Codes | When |
|---|---|---|
| 400 | Malformed request — e.g. the | |
| 401 | No credential, or one that is not valid | |
| 402 | Refused because of money: | |
| 403 | Authenticated, but not allowed: scope ( | |
| 404 | No such resource for this tenant. Somebody else's resource — and a resource of a project the caller does not reach — answers the same way on purpose: the two must not be distinguishable. | |
| 409 | State conflict: idempotency clash, stale draft, illegal transition; the last owner of an organization ( | |
| 422 | Validation failure or a semantically impossible request. Field-level validation from the framework answers | |
| 429 | Rate limited for this API key | |
| 500 | The walk broke and the execution is closed as | |
| 502 | The walk broke and the execution is closed as | |
| 503 | The walk broke and the execution is closed as | |
| 504 | The walk broke and the execution is closed as |
Headers
X-Request-IdRetry-After429
The body of every error is the error envelope.
List operations
GET /v1/operations
- Scope:
read
Parameters
| Parameter | Description |
|---|---|
cursor optionalquery | The |
limit optionalquery | Page size (default 30).
|
page optionalquery | The page number, from 1. Turns the list into its numbered form: the answer's
|
anchor optionalquery | The
|
q optionalquery | Search. Digits that are the id of an operation of the project find exactly that operation, past the window — the id carries its own moment, so
|
operation_code optionalquery |
|
status optionalquery |
|
from optionalquery | Window start, ISO-8601 — a value with an offset is the moment it names, one without is read as UTC. The rule of the three lists that take this parameter (
|
to optionalquery | Window end, ISO-8601, inclusive. See
|
Responses
One page of operations — a cursor page, or a numbered one with page
{
"data": [
{
"operation_id": "74438200973594624",
"operation_code": "generate_email",
"external_user_id": "user_8421",
"status": "pending",
"attempts": [
{
"attempt_id": "74438200973594624",
"attempt_number": 1,
"connection_id": "74438200973594624",
"pool_id": "74438200973594624",
"model_code": "gpt-4o-mini",
"model_price_id": "74438200973594624",
"status": "pending",
"error_code": "string",
"provider_request_id": "74438200973594624",
"input_tokens": 1,
"output_tokens": 1,
"cost": "0.02700000",
"cost_multiplier": "string",
"latency_ms": 1,
"started_at": "2026-10-02T09:30:00Z",
"completed_at": "2026-10-02T09:30:00Z",
"pool_name": "string",
"connection_name": "string",
"connection_deleted": true,
"provider": "string",
"finish_reason": "string"
}
],
"created_at": "2026-10-02T09:30:00Z",
"updated_at": "2026-10-02T09:30:00Z",
"flow_execution": {
"execution_id": "74438200973594624",
"flow_id": "74438200973594624",
"node_key": "string"
}
}
],
"meta": {
"limit": 1,
"next_cursor": "string"
}
}Errors
| Status | Codes | When |
|---|---|---|
| 401 | No credential, or one that is not valid | |
| 403 | Authenticated, but not allowed: scope ( | |
| 422 | Validation failure or a semantically impossible request. Field-level validation from the framework answers |
Headers
X-Request-Id
The body of every error is the error envelope.
One request with every provider attempt
GET /v1/operations/{id}
- Scope:
read
The way to find out what an uncertain operation actually did.
Parameters
| Parameter | Description |
|---|---|
id requiredpath | Snowflake identifier, as a string.
|
Responses
The operation and its attempts
{
"data": {
"operation_id": "74438200973594624",
"operation_code": "generate_email",
"external_user_id": "user_8421",
"status": "pending",
"attempts": [
{
"attempt_id": "74438200973594624",
"attempt_number": 1,
"connection_id": "74438200973594624",
"pool_id": "74438200973594624",
"model_code": "gpt-4o-mini",
"model_price_id": "74438200973594624",
"status": "pending",
"error_code": "string",
"provider_request_id": "74438200973594624",
"input_tokens": 1,
"output_tokens": 1,
"cost": "0.02700000",
"cost_multiplier": "string",
"latency_ms": 1,
"started_at": "2026-10-02T09:30:00Z",
"completed_at": "2026-10-02T09:30:00Z",
"pool_name": "string",
"connection_name": "string",
"connection_deleted": true,
"provider": "string",
"finish_reason": "string"
}
],
"created_at": "2026-10-02T09:30:00Z",
"updated_at": "2026-10-02T09:30:00Z",
"flow_execution": {
"execution_id": "74438200973594624",
"flow_id": "74438200973594624",
"node_key": "string"
}
}
}Errors
| Status | Codes | When |
|---|---|---|
| 401 | No credential, or one that is not valid | |
| 403 | Authenticated, but not allowed: scope ( | |
| 404 | No such resource for this tenant. Somebody else's resource — and a resource of a project the caller does not reach — answers the same way on purpose: the two must not be distinguishable. |
Headers
X-Request-Id
The body of every error is the error envelope.
List reservations
GET /v1/authorizations
- Scope:
read
Parameters
| Parameter | Description |
|---|---|
cursor optionalquery | The |
limit optionalquery | Page size (default 30).
|
page optionalquery | The page number, from 1. Turns the list into its numbered form: the answer's
|
anchor optionalquery | The
|
operation_code optionalquery |
|
from optionalquery | Window start, ISO-8601 — a value with an offset is the moment it names, one without is read as UTC. The rule of the three lists that take this parameter (
|
to optionalquery | Window end, ISO-8601, inclusive. See
|
Responses
One page of reservations — a cursor page, or a numbered one with page
{
"data": [
{
"reservation_id": "74438200973594624",
"status": "authorized",
"model_code": "gpt-4o-mini",
"operation_code": "generate_email",
"external_user_id": "user_8421",
"idempotency_key": "order-42:email",
"estimated_cost": "0.02700000",
"reserved_amount": "0.02700000",
"captured_amount": "0.02700000",
"overrun_amount": "0.02700000",
"estimated_input_tokens": 1,
"estimated_output_tokens": 1,
"max_output_tokens": 1,
"safety_multiplier": "string",
"estimation_strategy": "fixed",
"model_price_id": "74438200973594624",
"customer_hold_id": "74438200973594624",
"customer_hold_credits": "12.5000",
"currency": "USD",
"expires_at": "2026-10-02T09:30:00Z",
"created_at": "2026-10-02T09:30:00Z"
}
],
"meta": {
"limit": 1,
"next_cursor": "string"
}
}Errors
| Status | Codes | When |
|---|---|---|
| 401 | No credential, or one that is not valid | |
| 403 | Authenticated, but not allowed: scope ( | |
| 422 | Validation failure or a semantically impossible request. Field-level validation from the framework answers |
Headers
X-Request-Id
The body of every error is the error envelope.
Examples
Run a flow
The flow bound to generate_email answered at once, so the status is completed, and output holds what the flow returned. cost.actual is null here: the model call is settled after the answer.
curl https://api.slen.ai/api/v1/operations/execute \
-H "Authorization: Bearer $SLEN_API_KEY" \
-H "Idempotency-Key: order-42:email" \
-H "Content-Type: application/json" \
-d '{
"operation": "generate_email",
"input": {
"messages": [
{
"role": "user",
"content": "Write a short follow-up email."
}
]
}
}'{
"data": {
"execution_id": "74438200973594700",
"status": "completed",
"operation": "generate_email",
"output": {
"model": "gpt-4o",
"content": "Hi Ada, just checking in on the proposal I sent last week.",
"finish_reason": "stop",
"tool_calls": null,
"usage": {
"input_tokens": 14,
"output_tokens": 16,
"cached_input_tokens": 0
}
},
"error": null,
"error_code": null,
"error_message": null,
"cost": {
"estimated": "0.00412500",
"actual": null
},
"cache": {
"hit": false,
"cached_at": null
},
"replayed": false,
"waiting_on": null,
"trace_url": "/v1/executions/74438200973594700"
}
}Find the calls whose outcome is unknown
The filter keeps the uncertain operations of one operation code whose customer ID starts with user_84, on the first numbered page. One call of user_8421 is among them.
curl "https://api.slen.ai/api/v1/operations?q=user_84&operation_code=generate_email&status=uncertain&page=1" \
-H "Authorization: Bearer $SLEN_API_KEY"{
"data": [
{
"operation_id": "74442181054119936",
"operation_code": "generate_email",
"external_user_id": "user_8421",
"status": "uncertain",
"attempts": [],
"created_at": "2026-10-02T09:12:04+00:00",
"updated_at": "2026-10-02T09:13:04+00:00"
}
],
"meta": {
"limit": 30,
"page": 1,
"total": 1,
"total_capped": false,
"anchor": "74442181054119936"
}
}Read one call with its attempts
The call had one attempt, through the pool Primary. The connection did not answer in time, so the attempt is uncertain and carries no tokens, no cost and no finish_reason. The call is the model call of the node model of a flow, and flow_execution names its execution.
curl https://api.slen.ai/api/v1/operations/74442181054119936 \
-H "Authorization: Bearer $SLEN_API_KEY"{
"data": {
"operation_id": "74442181054119936",
"operation_code": "generate_email",
"external_user_id": "user_8421",
"status": "uncertain",
"attempts": [
{
"attempt_id": "74442181058314240",
"attempt_number": 1,
"connection_id": "74438912450035712",
"pool_id": "74438955400675328",
"model_code": "gpt-4o-mini",
"model_price_id": "1108",
"status": "uncertain",
"error_code": "operation_uncertain",
"provider_request_id": null,
"input_tokens": null,
"output_tokens": null,
"cost": null,
"cost_multiplier": "1.0000",
"latency_ms": 60004,
"started_at": "2026-10-02T09:12:04+00:00",
"completed_at": "2026-10-02T09:13:04+00:00",
"pool_name": "Primary",
"connection_name": "OpenAI production",
"connection_deleted": false,
"provider": "openai",
"finish_reason": null
}
],
"created_at": "2026-10-02T09:12:04+00:00",
"updated_at": "2026-10-02T09:13:04+00:00",
"flow_execution": {
"execution_id": "74442180941398016",
"flow_id": "74438623450128384",
"node_key": "model"
}
}
}List the holds of an operation
The request keeps the holds of generate_email over the last seven days, twenty to a page.
curl "https://api.slen.ai/api/v1/authorizations?operation_code=generate_email&limit=20" \
-H "Authorization: Bearer $SLEN_API_KEY"Where to go next
Send a request — the same request from the SDK, and every field of its answer.
Idempotency — what a repeat of a request to a flow is answered with.
Async runs and waiting — read an execution that answered
202.Ledger entries — the money each call held and captured.
How budget reservations work — what a hold does to a budget, and how it ends.
AI flows — how an operation code is bound to a flow.
Executions — the status and the trace of the execution a request opened.
Built from openapi b070e1f, sdk contract 6a2e0e0, php 4089db3, node 0fa87be