Build, publish and bind a flow

Create a flow, save its draft, check and publish it, roll it back, give it an operation code, and read its executions and analytics.

A flow is the graph of nodes a request runs: a request names an operation code, and Slen walks the flow bound to that code. The operations of this page create a flow, edit its draft, check and publish it, roll it back, bind it to an operation code, and read its executions and analytics. The console shows the same flows on AI Flows; AI flows and Publish, versions and rollback describe them there.

Who may change a flow

Reading a flow, its draft, its versions and its executions takes management:read, and its analytics take read. Everything that changes a flow takes flows:write: creating and archiving it, saving the draft, publishing, rolling back and binding the operation code.

A flow belongs to one project. A key reads and changes the flows of its own project. A flow of another project, or one that does not exist, answers 404 with the code flow_not_found.

Create and archive

  • A flow is created in the project of the key, with a name of up to 120 characters. Its slug is made from the name, and a slug the project already uses gets a number at its end.

  • A new flow has no version that runs: its status is draft until the first publish, and active after it.

  • status set to archived in the update stops the flow. A request for its operation code is answered 404 with the code flow_not_found, and its binding and its active version are kept. status set to active brings it back as it was, and so does a publish.

The draft and its revision

A flow is edited as a draft and runs as a published version. GET /v1/flows/{id}/draft answers with the graph, its version_number and draft_rev, and the settings this installation checks graphs with — among them the node types a new version may not hold. A flow with no draft answers with the graph of the version that runs, from_published set to true, and base_version naming that version.

The save sends the whole graph back with the draft_rev it read.

  • A draft_rev that is not the current one is refused with 409 and the code draft_conflict, and the graph is not written: somebody saved in between. Read the draft again.

  • A caller that holds the graph on screen also sends version_number, since every draft counts its revisions from zero. The first save of a flow with no draft also sends base_version. Both are refused with the same 409 when the draft is no longer the one they name.

  • A save checks only the shape of the graph. What the graph means is judged by the check and by a publish.

Check and publish

  • POST /v1/flows/{id}/validate checks the draft, or the version that runs when there is no draft, and changes nothing. valid says whether a publish would pass, issues lists the errors and the warnings, and source says which graph was checked.

  • A publish turns the draft into the next numbered version and makes it the one requests run. A draft with an error is refused with 422 and the code flow_invalid, and error.details.issues lists what was found. A warning does not stop it.

  • A flow with no draft answers 404 with the code flow_draft_not_found: there is nothing new to publish.

  • A save or another publish that lands while the publish is prepared refuses it with 409 and the code draft_conflict, and nothing is published. A publish may name its draft with version_number and draft_rev, to be refused the same way when the draft has moved since it was read.

  • A node of a frozen type stops a publish: the issue node_type_frozen names the node, and params.type its type. Http Request is frozen unless the service is set up otherwise. A version published with such a node goes on running, and a rollback can return to it.

How a flow is built lists what every flow must have. Slen sends the webhook event flow.published after a publish.

Roll back

A rollback makes an earlier published version run again. With version_number it goes to that version. Without it the flow goes to the published version just below the active one, so repeated calls walk back through the history.

  • A version the flow never had answers 404 with the code flow_version_not_found, and a draft 409 with the code flow_version_not_published.

  • With no published version below the active one, the answer is 409 with the code no_previous_version.

  • A version that names a model or a provider pool that is gone since is refused with 422 and the code flow_invalid.

The answer is the version that runs now. The draft is left as it is, and Slen sends the webhook event flow.rolled_back.

The operation code

A request reaches a flow through its operation code, and a flow answers to one code. The binding replaces the code the flow had: from the next request the old code is answered 404 with the code flow_not_found, and it is free for another flow. A code that another flow of the project has answers 409 with the code operation_already_bound, and the flow keeps its own code. Removing the binding leaves the flow without a code.

Executions and analytics

  • The executions of a flow are listed the newest first and read page by page: with cursor, or by number — page with limit also answers total, and anchor, the id the first page answered, keeps the next pages from shifting while new executions come in. q finds one execution by its whole id, digits only; the id of an execution of another flow answers an empty page. status narrows the list, and from and to narrow it to the executions started between them. Either end may be sent alone, and without them the list goes through the whole history. A value with an offset is the moment it names; a value without one is read as UTC. path keeps the executions that walked exactly one path — the node keys of an entry of requests_by_path joined by > — and is taken only with from and to.

  • The analytics of a flow are a row for each node by default. With granularity=flow the answer is the dashboard of the flow: its requests, costs and outcomes over the window. The window follows the rules of a report — Analytics: at most 31 days for the rows of the nodes, and 366 days for the dashboard. The dashboard is counted over whole hours that have closed: the window of the answer ends at the start of the current hour, or earlier while the last hour is still being reconciled. Each entry of requests_by_path carries how its executions ended (status) and the types of its nodes, and each entry of cost_by_connection the pool the calls went through (pool_id).

Operations

List flows

GET /v1/flows

  • Scope: management:read

Parameters

ParameterDescription
limit optional

queryinteger

Page size (default 30).

  • Minimum: 1
  • Maximum: 100
cursor optional

querystring

The meta.next_cursor of the previous page.

status optional

queryFlowStatus

  • One of: draft, active, archived
project optional

querystring

current narrows the list to the active project — an API key's own, a person's as switched on POST /api/auth/me/project — plus the organization's rows where the resource has them. A literal, never an id: anything else is 422. Without it the list is everything within the caller's reach.

  • One of: current

Responses

200

One cursor page of flows

application/json
{
  "data": [
    {
      "flow_id": "74438200973594624",
      "organization_id": "74438200973594624",
      "project_id": "74438200973594624",
      "name": "string",
      "slug": "string",
      "status": "draft",
      "active_version": {
        "version_id": "74438200973594624",
        "version_number": 1,
        "status": "draft",
        "draft_rev": 1,
        "published_at": "2026-10-02T09:30:00Z",
        "published_by": "string",
        "published_by_user_id": "74438200973594624",
        "created_at": "2026-10-02T09:30:00Z"
      },
      "draft_version": {
        "version_id": "74438200973594624",
        "version_number": 1,
        "status": "draft",
        "draft_rev": 1,
        "published_at": "2026-10-02T09:30:00Z",
        "published_by": "string",
        "published_by_user_id": "74438200973594624",
        "created_at": "2026-10-02T09:30:00Z"
      },
      "operation_codes": [
        "string"
      ],
      "created_at": "2026-10-02T09:30:00Z",
      "updated_at": "2026-10-02T09:30:00Z"
    }
  ],
  "meta": {
    "limit": 1,
    "next_cursor": "string"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Create a flow

POST /v1/flows

  • Scope: flows:write

The flow is created in the caller's organization, in project_id when given — which must be a project of that organization the caller reaches, or 404 project_not_found and nothing is stored — else in the caller's active project.

The slug is derived from name and is unique inside the project: letters outside Latin are transliterated (Cyrillic, Ukrainian included — Первый флоу → pervyy-flou), every other run of non-alphanumerics becomes one hyphen, 120 characters at most, flow when nothing is left. An ASCII name gives the slug it always gave. A slug the project already uses gets the first free numeric suffix (support-answer-2, -3, …) instead of a refusal; 409 flow_slug_taken remains only for two creates racing for the same free slug — retrying succeeds.

Request body

application/json
{
  "name": "string",
  "project_id": "74438200973594624"
}

Responses

201

The created flow

application/json
{
  "data": {
    "flow_id": "74438200973594624",
    "organization_id": "74438200973594624",
    "project_id": "74438200973594624",
    "name": "string",
    "slug": "string",
    "status": "draft",
    "active_version": {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z"
    },
    "draft_version": {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z"
    },
    "operation_codes": [
      "string"
    ],
    "created_at": "2026-10-02T09:30:00Z",
    "updated_at": "2026-10-02T09:30:00Z"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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 (last_owner); the owned-organization limit (organization_limit_reached); an end customer with credit history that cannot be deleted (customer_has_history)

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Read one flow with its active and draft versions

GET /v1/flows/{id}

  • Scope: management:read

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Responses

200

The flow

application/json
{
  "data": {
    "flow_id": "74438200973594624",
    "organization_id": "74438200973594624",
    "project_id": "74438200973594624",
    "name": "string",
    "slug": "string",
    "status": "draft",
    "active_version": {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z"
    },
    "draft_version": {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z"
    },
    "operation_codes": [
      "string"
    ],
    "created_at": "2026-10-02T09:30:00Z",
    "updated_at": "2026-10-02T09:30:00Z"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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.

Rename or archive a flow

PATCH /v1/flows/{id}

  • Scope: flows:write

An archived flow is not executed: POST /v1/operations/execute for its operation answers 404 flow_not_found. Its binding and its active version are kept, so status: active brings it back as it was, and a publish un-archives it as well. An execution parked before the flow was archived still resumes and finishes on its own version.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Request body

application/json
{
  "name": "string",
  "status": "active"
}

Responses

200

The updated flow

application/json
{
  "data": {
    "flow_id": "74438200973594624",
    "organization_id": "74438200973594624",
    "project_id": "74438200973594624",
    "name": "string",
    "slug": "string",
    "status": "draft",
    "active_version": {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z"
    },
    "draft_version": {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z"
    },
    "operation_codes": [
      "string"
    ],
    "created_at": "2026-10-02T09:30:00Z",
    "updated_at": "2026-10-02T09:30:00Z"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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.

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

The editable graph

GET /v1/flows/{id}/draft

  • Scope: management:read

Beside the graph and its revision, the settings this installation validates and runs graphs with (FlowBuilderSettings), for the builder to say.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Responses

200

The draft graph, its revision and the builder's settings

application/json
{
  "data": {
    "version_number": 1,
    "draft_rev": 1,
    "from_published": true,
    "base_version": 1,
    "graph": {
      "version": 1,
      "nodes": [
        {
          "key": "string",
          "type": "string",
          "label": "string",
          "config": {},
          "position": {
            "x": 1,
            "y": 1
          }
        }
      ],
      "edges": [
        {
          "key": "string",
          "source": "string",
          "source_port": "string",
          "target": "string",
          "label": "string"
        }
      ]
    },
    "settings": {
      "max_output_tokens": 1,
      "max_parked_hours": 1,
      "park_longest_wait_hours": 1,
      "frozen_node_types": [
        "sdk_request"
      ]
    }
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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.

Save the draft graph

PUT /v1/flows/{id}/draft

  • Scope: flows:write

draft_rev is the optimistic-lock token read from the draft; a stale one is refused (409 draft_conflict).

draft_rev counts from zero in every draft, so alone it does not tell the draft of one version from the draft of the next. A caller that holds a graph on screen — the builder — also sends the version_number that GET /draft (or its last save) answered: when the draft is of another version — the one it read was published elsewhere and edited again — the save is refused with 409 draft_conflict even when the two revisions coincide, and the graph is not written. Without version_number only the revision is compared, as before.

A flow with no draft is edited from the version that runs, and a rollback replaces that graph without moving either number. The first save of such a flow therefore also names the base_version that GET /draft answered: when the working graph is read from another version by now, or is somebody's draft already, the save is refused with 409 draft_conflict. A refused save of a flow with no draft creates no draft. A caller that names no base is not held to one.

A pool_id in the graph that the caller does not reach is an unknown pool to validation (unknown_pool), exactly like one that does not exist. A pool_id that is not text — a number, a list, an object — is invalid_node_config with the detail ai_model_invalid_pool_id: the runtime routes through a named pool only for a non-empty string. A pool that is switched off is the warning pool_disabled: the graph can be saved and published, and every call of that node is refused provider_not_configured until the pool is enabled.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Request body

application/json
{
  "graph": {
    "version": 1,
    "nodes": [
      {
        "key": "string",
        "type": "string",
        "label": "string",
        "config": {},
        "position": {
          "x": 1,
          "y": 1
        }
      }
    ],
    "edges": [
      {
        "key": "string",
        "source": "string",
        "source_port": "string",
        "target": "string",
        "label": "string"
      }
    ]
  },
  "draft_rev": 1,
  "version_number": 1,
  "base_version": 1
}

Responses

200

The draft version after the save

application/json
{
  "data": {
    "version_id": "74438200973594624",
    "version_number": 1,
    "status": "draft",
    "draft_rev": 1,
    "published_at": "2026-10-02T09:30:00Z",
    "published_by": "string",
    "published_by_user_id": "74438200973594624",
    "created_at": "2026-10-02T09:30:00Z"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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 (last_owner); the owned-organization limit (organization_limit_reached); an end customer with credit history that cannot be deleted (customer_has_history)

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Check the working graph without publishing it

POST /v1/flows/{id}/validate

  • Scope: management:read

Checks the draft when there is one and the version that runs otherwise — the active one, which after a rollback is not the latest published; source in the answer says which. 404 only when the flow has neither.

The body is optional. A caller that is looking at a graph — an editor with the draft on screen — sends back the version_number and draft_rev that GET /draft (or its last save) answered, and for a flow with no draft its base_version as well: when the working graph is no longer that one, the answer is 409 draft_conflict instead of a verdict about a graph the caller has not seen. Without the body whatever the server holds is checked.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Request body optional

application/json
{
  "version_number": 1,
  "draft_rev": 1,
  "base_version": 1
}

Responses

200

Validation issues, if any

application/json
{
  "data": {
    "valid": true,
    "issues": [
      {
        "severity": "error",
        "code": "invalid_graph_shape",
        "message": "string",
        "node_key": "string",
        "edge_key": "string",
        "params": {
          "key": "string"
        }
      }
    ],
    "source": "draft"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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 (last_owner); the owned-organization limit (organization_limit_reached); an end customer with credit history that cannot be deleted (customer_has_history)

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Make the draft the version that runs

POST /v1/flows/{id}/publish

  • Scope: flows:write

The version is frozen at the draft revision that was validated and compiled. A draft save, or another publish of the same draft, that lands while this one is being prepared is 409 draft_conflict: nothing is published and no flow.published is sent — read the draft again and repeat the call. 404 flow_draft_not_found when the flow has no draft (it was already published and not edited since).

That check sees only what changes *during* the call. A save that landed before it — another tab, another member — is simply the draft, and it is what gets published. A caller that publishes the graph it is looking at sends the optional body: the version_number and draft_rev that GET /draft (or its last save) answered. When the draft is another version or at another revision the answer is the same 409 draft_conflict, before the graph is validated. A body with any other field, or with a value that is not such a number, is 422.

A draft that holds a node of a frozen type is refused like any other invalid graph: 422 flow_invalid that carries a node_type_frozen issue, with the node in node_key and the type in params.type. Which types are frozen is the deployment's setting; http_request is by default. The freeze is about new versions only: a version published with such a node keeps running, an execution parked on it resumes, and a rollback onto it works — but its graph cannot be published again until the node is taken out, and POST /validate says so (valid: false) for a flow whose running version holds one.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Request body optional

application/json
{
  "version_number": 1,
  "draft_rev": 1
}

Responses

200

The published version and any warnings

application/json
{
  "data": {
    "version": {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z"
    },
    "warnings": [
      {
        "severity": "error",
        "code": "invalid_graph_shape",
        "message": "string",
        "node_key": "string",
        "edge_key": "string",
        "params": {
          "key": "string"
        }
      }
    ]
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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 (last_owner); the owned-organization limit (organization_limit_reached); an end customer with credit history that cannot be deleted (customer_has_history)

422

The graph cannot be published. error.details.issues is the whole validation result of the draft, in the shape /validate answers (ValidationIssue): the errors that block the publish first, then the warnings found beside them — tell them apart by severity. The first item is always an error. A body that is not a FlowDraftExpectation — an unknown field, a value that is not such a number — is refused before the draft is read, in the framework's field-level {"message", "errors"} form

Headers

  • X-Request-Id

The body of every error is the error envelope.

Go back to an earlier published version

POST /v1/flows/{id}/rollback

  • Scope: flows:write

Omitting version_number means the published version next below the live one, so repeated calls walk back through the history (version 5, then 4, then 3); going forward again is a rollback by number.

A draft somebody has begun is left alone. A flow with no draft is edited from the version that is live after the rollback.

The answer is the version that is live now — a FlowVersion, not the PublishResult of a publish: a rollback re-checks references only and has no warnings to report. The switch and its flow.rolled_back event are one transaction.

Refusals: 404 flow_version_not_found — the flow has no version of that number; 409 flow_version_not_published — the version named is a draft; 409 no_previous_version — no number was given and there is no published version earlier than the live one (or the flow has no live version). 422 is one of two things: a version_number that is not a positive integer (the field-level {"message", "errors"} answer), or flow_invalid in the error envelope with details.issues, as on a publish — a model or a pool the target version refers to is gone, so it can no longer run.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Request body optional

application/json
{
  "version_number": 1
}

Responses

200

The version now active

application/json
{
  "data": {
    "version_id": "74438200973594624",
    "version_number": 1,
    "status": "draft",
    "draft_rev": 1,
    "published_at": "2026-10-02T09:30:00Z",
    "published_by": "string",
    "published_by_user_id": "74438200973594624",
    "created_at": "2026-10-02T09:30:00Z"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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 (last_owner); the owned-organization limit (organization_limit_reached); an end customer with credit history that cannot be deleted (customer_has_history)

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Every version of a flow

GET /v1/flows/{id}/versions

  • Scope: management:read

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Responses

200

Versions, newest first

application/json
{
  "data": [
    {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z",
      "published_by_name": "string"
    }
  ]
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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.

One version whole — the summary plus the graph it froze (the policy diff reads two of these)

GET /v1/flows/{id}/versions/{version}

  • Scope: management:read

The version of the flow with this number: the summary GET /v1/flows/{id}/versions lists, plus the graph it froze, in the shape the draft endpoint uses. A number the flow never had is 404 flow_version_not_found; a flow out of reach is 404 flow_not_found.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624
version required

pathinteger

  • Minimum: 1

Responses

200

The version with its graph, in the draft endpoint's shape

application/json
{
  "data": {
    "version_id": "74438200973594624",
    "version_number": 1,
    "status": "draft",
    "draft_rev": 1,
    "published_at": "2026-10-02T09:30:00Z",
    "published_by": "string",
    "published_by_user_id": "74438200973594624",
    "created_at": "2026-10-02T09:30:00Z",
    "graph": {
      "version": 1,
      "nodes": [
        {
          "key": "string",
          "type": "string",
          "label": "string",
          "config": {},
          "position": {
            "x": 1,
            "y": 1
          }
        }
      ],
      "edges": [
        {
          "key": "string",
          "source": "string",
          "source_port": "string",
          "target": "string",
          "label": "string"
        }
      ]
    }
  }
}

Errors

StatusCodesWhen
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.

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Give this flow its operation code

PUT /v1/flows/{id}/binding

  • Scope: flows:write

The binding is what POST /v1/operations/execute resolves by. A flow answers to one operation code: the PUT replaces the code the flow had, in one transaction. The previous code answers 404 flow_not_found to POST /v1/operations/execute from the next call and is free for another flow; the code the flow already has is answered as it is. 409 operation_already_bound — the code belongs to another flow of the project, and this flow keeps the code it had.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Request body

application/json
{
  "operation_code": "generate_email"
}

Responses

200

The flow with its operation code

application/json
{
  "data": {
    "flow_id": "74438200973594624",
    "organization_id": "74438200973594624",
    "project_id": "74438200973594624",
    "name": "string",
    "slug": "string",
    "status": "draft",
    "active_version": {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z"
    },
    "draft_version": {
      "version_id": "74438200973594624",
      "version_number": 1,
      "status": "draft",
      "draft_rev": 1,
      "published_at": "2026-10-02T09:30:00Z",
      "published_by": "string",
      "published_by_user_id": "74438200973594624",
      "created_at": "2026-10-02T09:30:00Z"
    },
    "operation_codes": [
      "string"
    ],
    "created_at": "2026-10-02T09:30:00Z",
    "updated_at": "2026-10-02T09:30:00Z"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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 (last_owner); the owned-organization limit (organization_limit_reached); an end customer with credit history that cannot be deleted (customer_has_history)

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Remove the operation binding

DELETE /v1/flows/{id}/binding

  • Scope: flows:write

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Responses

204

Unbound

No body

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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.

Runs of one flow

GET /v1/flows/{id}/executions

  • Scope: management:read

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624
limit optional

queryinteger

Page size (default 30).

  • Minimum: 1
  • Maximum: 100
cursor optional

querystring

The meta.next_cursor of the previous page.

page optional

queryinteger

The page number, from 1. Turns the list into its numbered form: the answer's meta is a PageMeta (with total) instead of a CursorMeta. Together with cursor it is a 422; without page the answer is the cursor page, unchanged. A page past the last one is an empty data with the same total.

  • Minimum: 1
  • Maximum: 10000
anchor optional

querystring

The meta.anchor of the first numbered page: only rows with an id at or below it are counted and paged, so rows written after the first page do not shift pages 2..N. Without it the newest matching row is the anchor, and the answer names it. Only with page (422 otherwise).

  • Pattern: ^\d{1,20}$
status optional

queryExecutionStatus

  • One of: running, queued, waiting, completed, rejected, failed, uncertain
from optional

querystring

Only runs started at or after this moment. Either end may be given alone; there is no cap on the width and no default window — without either the list pages through the whole history. A value with an offset is the moment it names; one without is read as UTC. A value that is not a date is a field-validation 422.

  • Format: date-time
to optional

querystring

Only runs started at or before this moment; see from.

  • Format: date-time
q optional

querystring

Search by the full id of an execution — digits only, anything else is a field-validation 422. Found only among the runs of this flow in the caller's organization: the id of another flow's, project's or organization's run is an empty data (and total: 0), never a 404. Combines with status, from / to and page. An empty value is no search.

  • Pattern: ^\d{1,20}$
path optional

querystring

Only the runs that walked exactly this path: the node keys in the order they were entered, joined by > — requests_by_path[].path of GET /v1/flows/{id}/analytics?granularity=flow joined the same way. Only together with from and to (without them a field-validation 422 on from / to), and read like the dashboard's hours: a run counts once it has finished, and to is exclusive. With the dashboard's window and the entry's status, the count is the entry's requests. Combines with every other filter and with page. The runs of the last 90 days are kept.

  • Pattern: ^[a-z0-9_-]{1,64}(>[a-z0-9_-]{1,64})*$
  • Maximum length: 4000

Responses

200

One page of executions — a cursor page, or a numbered one with page

application/json
{
  "data": [
    {
      "execution_id": "74438200973594624",
      "flow_id": "74438200973594624",
      "flow_version_id": "74438200973594624",
      "operation": "generate_email",
      "operation_id": "74438200973594624",
      "status": "running",
      "error_code": "string",
      "error_message": "string",
      "cost": {
        "estimated": "0.02700000",
        "actual": "0.02700000"
      },
      "cache": {
        "hit": true,
        "cached_at": "2026-10-02T09:30:00Z"
      },
      "started_at": "2026-10-02T09:30:00Z",
      "finished_at": "2026-10-02T09:30:00Z",
      "duration_ms": 1
    }
  ],
  "meta": {
    "limit": 1,
    "next_cursor": "string"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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.

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Per-node stats, or the flow dashboard

GET /v1/flows/{id}/analytics

  • Scope: read

granularity=node (the default) reads the raw node executions of the window; granularity=flow reads the hourly rollup and answers the dashboard. The two shapes differ.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624
from optional

querystring

Start of the window an analytics answer covers — a date the server can parse, ISO-8601 recommended (a value without an offset is read as UTC). Unlike the lists, each end is optional on its own and the window is never refused, only fitted (AnalyticsWindowResolver):

  • Format: date-time
to optional

querystring

End of the window an analytics answer covers; now when absent. See from (WindowFrom) for how the window is fitted rather than refused.

  • Format: date-time
granularity optional

querystring

  • One of: node, flow
  • Default: node

Responses

200

Node rows or the flow dashboard, depending on granularity

application/json
{
  "data": [
    {
      "node_key": "string",
      "node_type": "string",
      "executions": 1,
      "failed": 1,
      "error_rate": "string",
      "cost": "0.02700000",
      "avg_duration_ms": 1,
      "p95_duration_ms": 1,
      "ports": {
        "key": 1
      },
      "saved_cost": "0.02700000"
    }
  ]
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

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.

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Examples

List the flows

The active flows of the project.

Terminal
curl "https://api.slen.ai/api/v1/flows?status=active" \
  -H "Authorization: Bearer $SLEN_API_KEY"
200 OK
{
  "data": [
    {
      "flow_id": "74438623450128384",
      "organization_id": "74438200973594112",
      "project_id": "74438200973594624",
      "name": "Follow-up email",
      "slug": "follow-up-email",
      "status": "active",
      "active_version": {
        "version_id": "74440987364098048",
        "version_number": 3,
        "status": "published",
        "draft_rev": 7,
        "published_at": "2026-10-01T15:40:22+00:00",
        "published_by": "74438344587730944",
        "published_by_user_id": null,
        "created_at": "2026-10-01T14:02:51+00:00"
      },
      "draft_version": null,
      "operation_codes": [
        "generate_email"
      ],
      "created_at": "2026-09-30T11:20:05+00:00",
      "updated_at": "2026-10-01T15:40:22+00:00"
    }
  ],
  "meta": {
    "limit": 30,
    "next_cursor": null
  }
}

Create a flow

The new flow is in the project of the key and has no version yet.

Terminal
curl https://api.slen.ai/api/v1/flows \
  -H "Authorization: Bearer $SLEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support answer"
  }'
201 Created
{
  "data": {
    "flow_id": "74443519874433024",
    "organization_id": "74438200973594112",
    "project_id": "74438200973594624",
    "name": "Support answer",
    "slug": "support-answer",
    "status": "draft",
    "active_version": null,
    "draft_version": null,
    "operation_codes": [],
    "created_at": "2026-10-02T10:20:44+00:00",
    "updated_at": "2026-10-02T10:20:44+00:00"
  }
}

Read a flow

The answer is the flow with its active version and its draft.

Terminal
curl https://api.slen.ai/api/v1/flows/74438623450128384 \
  -H "Authorization: Bearer $SLEN_API_KEY"
200 OK
{
  "data": {
    "flow_id": "74438623450128384",
    "organization_id": "74438200973594112",
    "project_id": "74438200973594624",
    "name": "Follow-up email",
    "slug": "follow-up-email",
    "status": "active",
    "active_version": {
      "version_id": "74440987364098048",
      "version_number": 3,
      "status": "published",
      "draft_rev": 7,
      "published_at": "2026-10-01T15:40:22+00:00",
      "published_by": "74438344587730944",
      "published_by_user_id": null,
      "created_at": "2026-10-01T14:02:51+00:00"
    },
    "draft_version": {
      "version_id": "74443540644626432",
      "version_number": 4,
      "status": "draft",
      "draft_rev": 2,
      "published_at": null,
      "published_by": null,
      "published_by_user_id": null,
      "created_at": "2026-10-02T10:25:13+00:00"
    },
    "operation_codes": [
      "generate_email"
    ],
    "created_at": "2026-09-30T11:20:05+00:00",
    "updated_at": "2026-10-02T10:31:09+00:00"
  }
}

Archive a flow

Requests for the operation code of the flow are refused until it is active again.

Terminal
curl -X PATCH https://api.slen.ai/api/v1/flows/74438623450128384 \
  -H "Authorization: Bearer $SLEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "archived"
  }'
200 OK
{
  "data": {
    "flow_id": "74438623450128384",
    "organization_id": "74438200973594112",
    "project_id": "74438200973594624",
    "name": "Follow-up email",
    "slug": "follow-up-email",
    "status": "archived",
    "active_version": {
      "version_id": "74440987364098048",
      "version_number": 3,
      "status": "published",
      "draft_rev": 7,
      "published_at": "2026-10-01T15:40:22+00:00",
      "published_by": "74438344587730944",
      "published_by_user_id": null,
      "created_at": "2026-10-01T14:02:51+00:00"
    },
    "draft_version": null,
    "operation_codes": [
      "generate_email"
    ],
    "created_at": "2026-09-30T11:20:05+00:00",
    "updated_at": "2026-10-02T11:02:37+00:00"
  }
}

Read the draft

The answer carries the graph, the two numbers a save sends back and the settings of the builder.

Terminal
curl https://api.slen.ai/api/v1/flows/74438623450128384/draft \
  -H "Authorization: Bearer $SLEN_API_KEY"

Save the draft

The graph of a request, one model call and the answer, saved at the revision it was read at.

Terminal
curl -X PUT https://api.slen.ai/api/v1/flows/74438623450128384/draft \
  -H "Authorization: Bearer $SLEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "graph": {
      "version": 1,
      "nodes": [
        {
          "key": "request",
          "type": "sdk_request",
          "label": null,
          "config": {},
          "position": {
            "x": 0,
            "y": 0
          }
        },
        {
          "key": "model",
          "type": "ai_model",
          "label": null,
          "config": {
            "model": "gpt-4o-mini",
            "max_output_tokens": 400
          },
          "position": {
            "x": 280,
            "y": 0
          }
        },
        {
          "key": "answer",
          "type": "return_response",
          "label": null,
          "config": {},
          "position": {
            "x": 560,
            "y": 0
          }
        }
      ],
      "edges": [
        {
          "key": "request-model",
          "source": "request",
          "source_port": "default",
          "target": "model"
        },
        {
          "key": "model-answer",
          "source": "model",
          "source_port": "success",
          "target": "answer"
        }
      ]
    },
    "draft_rev": 2,
    "version_number": 4
  }'

Check the draft

The body names the draft on screen. The answer says whether a publish would pass, and lists what was found.

Terminal
curl https://api.slen.ai/api/v1/flows/74438623450128384/validate \
  -H "Authorization: Bearer $SLEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "version_number": 4,
    "draft_rev": 3
  }'

Publish the draft

The body names the draft that was checked. A save made since then refuses the publish.

Terminal
curl https://api.slen.ai/api/v1/flows/74438623450128384/publish \
  -H "Authorization: Bearer $SLEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "version_number": 4,
    "draft_rev": 3
  }'

Roll back

Without a body the flow goes back to the version below the active one.

Terminal
curl -X POST https://api.slen.ai/api/v1/flows/74438623450128384/rollback \
  -H "Authorization: Bearer $SLEN_API_KEY"

List the versions

The versions of the flow, the newest first.

Terminal
curl https://api.slen.ai/api/v1/flows/74438623450128384/versions \
  -H "Authorization: Bearer $SLEN_API_KEY"

Read one version

The answer is the version with the graph it froze, in the shape of the draft.

Terminal
curl https://api.slen.ai/api/v1/flows/74438623450128384/versions/3 \
  -H "Authorization: Bearer $SLEN_API_KEY"

Bind the operation code

From the next request, generate_email runs this flow.

Terminal
curl -X PUT https://api.slen.ai/api/v1/flows/74438623450128384/binding \
  -H "Authorization: Bearer $SLEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "operation_code": "generate_email"
  }'
200 OK
{
  "data": {
    "flow_id": "74438623450128384",
    "organization_id": "74438200973594112",
    "project_id": "74438200973594624",
    "name": "Follow-up email",
    "slug": "follow-up-email",
    "status": "active",
    "active_version": {
      "version_id": "74440987364098048",
      "version_number": 3,
      "status": "published",
      "draft_rev": 7,
      "published_at": "2026-10-01T15:40:22+00:00",
      "published_by": "74438344587730944",
      "published_by_user_id": null,
      "created_at": "2026-10-01T14:02:51+00:00"
    },
    "draft_version": null,
    "operation_codes": [
      "generate_email"
    ],
    "created_at": "2026-09-30T11:20:05+00:00",
    "updated_at": "2026-10-01T15:40:22+00:00"
  }
}

Remove the binding

The answer has no body. A request for the old code is answered 404.

Terminal
curl -X DELETE https://api.slen.ai/api/v1/flows/74438623450128384/binding \
  -H "Authorization: Bearer $SLEN_API_KEY"

204 No Content

List the executions

The first page of 25 executions of the flow started since the morning of 2 October that ended failed.

Terminal
curl "https://api.slen.ai/api/v1/flows/74438623450128384/executions?status=failed&from=2026-10-02T06%3A00%3A00Z&page=1&limit=25" \
  -H "Authorization: Bearer $SLEN_API_KEY"

Read the analytics

The dashboard of the flow over the last seven days.

Terminal
curl "https://api.slen.ai/api/v1/flows/74438623450128384/analytics?granularity=flow" \
  -H "Authorization: Bearer $SLEN_API_KEY"

Where to go next

Built from openapi b070e1f, sdk contract 5cc7055, php ae4a53b, node 84bc62d