Files
sim/apps/docs/openapi-v2-logs.json
T
Waleed c930830310 fix(v2,cli): second audit pass over the v2 API and CLI (#7126)
* fix(chat): resolve a caller-supplied conversation id through its owner

The v2 chat route used the caller-supplied conversationId verbatim, with no
existence, owner, or workspace check, against a store keyed by bare text with
no owner column. A caller who knew another user's conversation id reached that
conversation. Ids now resolve through the same owner-scoped loader the web chat
path uses, and anything unresolvable answers one uniform 404 before any
lifecycle work runs. Omitting the id mints a server-issued conversation.

The contract also accepted any 1-128 character string for a column typed uuid,
so a malformed id raised a driver error and rendered 500 while an unknown but
well-formed id rendered 404 - a shape oracle, and a 500 on ordinary input.

The ownership predicate had no coverage anywhere: the route test mocked the
module and the lifecycle test drove a chain mock that ignores its where clause,
so deleting the owner condition left both suites green. It is now asserted by
composition and by condition count, which is what catches a dropped condition.

Also renames the reply's model identifier away from a term the project's own
copy rules forbid on a user-facing surface.

* fix(v2): conceal workspace absence, and stop archived tables faulting their page

Two reads answered a caller more than they were entitled to know.

A workspace a caller cannot reach at all returned FORBIDDEN while one that does
not exist returned NOT_FOUND, so a workspace-key holder could enumerate which
workspace ids exist by diffing the two. Both now answer the same absence, using
the concealment policy the billing routes already use. A refusal from inside the
workspace - a member whose role is too low - still answers FORBIDDEN, because
that caller already knows the workspace exists.

Separately, archiving a folder cascades onto its tables but leaves each table
pointing at the archived folder row. The archived listing resolved those paths
strictly, so one such row faulted the whole page and no cursor could step past
it - which also made the ids undiscoverable and left restore unreachable for
exactly the tables that need it. The archived scope now resolves leniently to
the root, where a restore would place them, matching the shipped workflows
behavior. Active listings still fault loudly on a dangling folder.

* fix(knowledge): validate upload processing options without stranding live sessions

recipe and lang were accepted as free strings up to their length caps, silently
discarded, and echoed back nowhere, so a typo was unobservable: uploading with a
misspelled recipe returned 200 and quietly used the default. Both are now
validated at the boundary and a bad value answers 400 naming what is accepted.

The accepted recipe set deliberately includes the sentinel every first-party
caller sends today alongside the three real chunker recipes, and the three are
derived from the chunker's own union so removing one there is a compile error
here rather than a silent 400 in production.

The same schema also parses metadata read back off a persisted upload session,
so tightening it would have thrown out of resume and complete for any session
created before this - a 500 on work that could then never finish. The read-back
path now drops a value it no longer recognises instead of rejecting it; the
request boundary stays strict.

Neither field reaches chunking, so nothing here moves chunk boundaries,
embeddings, or search results.

* fix(v2): honour a requested stats window, and answer a claimed graph id with a conflict

Log statistics accepted a start and an end, filtered the totals by them, and
then built the series against wall-clock now. Bucket width was computed over a
span the caller never asked for, and every bucket past the requested end was
structurally empty - so a bounded historical query returned a wrong-width series
with fabricated trailing buckets, under a window label that disagreed with the
request. Each edge now honours the bound it was given and keeps its previous
derivation when omitted, so an unbounded request is unchanged.

Separately, block, edge and subflow ids are global primary keys while the
delete that precedes a state replace is scoped to one workflow. An id owned by
another workflow survived that delete, the insert violated the key, and because
callers pass their own transaction the driver error escaped unclassified as a
server fault. The write now refuses such an id up front with a conflict naming
it, and re-classifies the same violation if one races past the check, since the
lock covers only the workflow being written. The dry run checks the ids a commit
would insert and reports the warnings a commit would report, which is what its
own contract already promised.

* fix(secrets): let a workspace secret change its metadata without resending the value

Restoring redaction cost more than removing it. The only way to flip a secret
back to redacted was to re-send the plaintext, because the write required a
value and omitting it fell into an interactive prompt that cannot run in CI.
A workspace secret can now change its description or visibility on its own; the
stored value is never re-encrypted or rewritten, a write that names no existing
secret answers not-found rather than creating one, and a personal secret still
requires a value because it has no other writable field.

The path parameter was also one shared schema across the write and the delete,
so a single description had to cover both and the delete documented an argument
that could create and replace. Split, mirroring the credentials pair.

The metadata write is a new update against the credentials table, so its scope
is asserted by composition and by condition count: an unscoped update would let
one workspace flip another workspace's identically-named secret out of
redaction, and the cache invalidation would then carry that flag into the other
workspace's runtime catalog.

* fix(v2): say what an error means in terms the caller can act on

A size-limit refusal collapsed every value under a kilobyte to "0 Bytes", so a
28-byte file over a 27-byte ceiling read "is 0 Bytes, above the 0 Bytes limit" -
self-contradictory, and useless for choosing a value that would work.

Errors and field descriptions also told callers to invoke raw HTTP endpoints.
These strings serve the REST reference and the CLI's own help equally, so they
now name the operation and its object rather than a method and a path. A sweep
test walks every v2 schema description and holds the line, with the remaining
offenders in files this change does not own recorded explicitly rather than
left to be rediscovered.

Listing the editors of a built-in skill claimed the skill did not exist, while
reading the same id succeeded - a well-formed request for a real resource is
not malformed, so the list answers an empty roster and only the mutations
refuse.

Bulk folder deletion recorded only the leaf name in its audit trail while the
single delete recorded the full path, leaving two same-named folders under
different parents indistinguishable after the fact.

Bulk chunk enable, disable and delete each treated an unmatched id differently
behind one sentence of documentation. They now follow one rule.

A workspace-scoped list refused with the name of a resource the caller never
addressed, which reads as an empty workspace rather than an unreachable one.

* fix(cli): stop a config value forging a section it was never meant to write

The config file is written by joining names and values into INI lines, and
nothing checked what was in them. A profile name carrying a newline and a
section header wrote a section that merged into a different profile and took
over its endpoint - and the next command sent that profile's stored API key
there. A workspace value could do the same from the other side, since only the
endpoint flag validated its input.

The refusal now lives at the writer, the single place untrusted text enters the
document, with the flag-level checks kept for the better message. Either alone
blocks the forgery; the pair is deliberate.

Rejecting rather than escaping, because the format has no escape syntax and
these files are hand-edited and read by other tools that would not decode one
we invented. The forbidden set covers control characters and the two Unicode
line separators, which the previous guard missed - those parse as an unreadable
line, so the key silently vanished on read and the next write appended a
duplicate while the command reported success.

Login also wrote the key before the settings, so a malformed response from the
deployment could leave a key on disk with no endpoint beside it, and the next
command would send it to the default host. Settings are written first, and the
response is checked before anything touches disk.

Name validation applies only when creating a profile, so a hand-written one
that predates the rule keeps working.

* fix(docs): tell the reader which key a command needs, and stop the ids contradicting the CLI

Around sixty v2 operations refuse a workspace API key, and the CLI's help said
nothing about it - the caller found out from a 403 after the request went out.
The restriction is already stated in the API spec, so the generator now reads it
from there and the command description carries it. The sentinel sentences are
imported from the spec's own constants rather than copied, so a reword cannot
silently unmark every command, and the test pins the count as well as named
operations because a reword confined to one family would otherwise slip past.

The generated reference also rendered an empty default as a sentence pointing at
nothing - "Defaults to ." - for every repeatable filter. Omitted now, while
false and zero still render, which is the trap that shape of check usually
walks into.

The hand-written guides used a workflow-shaped id for workflows that the CLI's
own help says never names one, and five other families were equally wrong. All
of them now match the scheme the CLI declares, consistently per entity across
pages, with the shared ones taken from that help text so the two read as one
voice.

The page documenting every flag was linked from nowhere; both landing links
pointed at the overview instead. And the generator's test file was absent from
the hand-maintained list CI runs, so its guards never executed.

* fix(cli): stop a page-size default capping a destructive filter

Every request field named limit inherited the pager's default of 100, but only
a cursor-paginated command interprets that flag. The two filter-based row
mutations declare no cursor, so the default went onto the wire as a row cap: a
filter matching 250 rows deleted 100, exited 0, and said nothing - while the
confirmation the user had just answered promised every matching row. The flag's
own help offered 0 for everything, which those endpoints reject; the unbounded
form is the field being absent. The pager's default now applies only where the
pager runs, and the tests pin the omission on the request body rather than in
help text.

A cap typed alongside an explicit row list was silently ignored; it is now
refused on the client, where refusing costs nothing to already-installed
versions.

Lists also truncated at a hundred with no signal in any format, and the two
inventory endpoints that do report truncation had that field dropped on the way
out - so a caller reconciling against a clipped list could not tell. One note
now goes to stderr while stdout stays a bare array, and a flag raised on a later
page survives the fold.

Also: a folder whose name contains the separator no longer prints a path that
resolves to a different folder; validation errors name the flag the user typed
instead of the wire field; an unknown subcommand with --help exits non-zero
instead of printing the parent's help; a fractional or negative page size is
refused rather than floored; an empty query filter is refused rather than
silently returning everything; and the two spellings of the missing-workspace
message became one.

* fix(cli): gate destructive table imports and fix follow-mode rendering

A `tables import --mode replace` empties the table before its first batch,
so the only warning was in the describe. It now confirms, and the wording
tells the truth per mode: cancelling a replace leaves a prefix of the new
file with the originals already gone, while an append re-adds its rows if
the file is imported twice. `--yes` skips the gate, and the gate runs
before the file is opened.

Import and export cancellation carried no describe at all; the import one
now confirms, the export one records why it deliberately does not.

Follow-mode output truncated cells to whatever the first row happened to
measure, so a longer status or workflow name arrived clipped with no
signal. Cells now clamp at a shared ceiling and pad to the lock, and the
log columns carry width floors so a short first page cannot pin a column
narrower than its own values.

Interrupting a staged download left the staging directory behind; it is
now removed on SIGINT and SIGTERM before the signal is re-raised.

`--select-output` without `--follow` selected from a response that does
not carry outputs, and said nothing. It is refused client-side, with a
separate message for `--async`. Its describe now names what the path
addresses.

`secrets set` always read a value, even when only metadata flags were
passed. Off a TTY that was an immediate refusal, so a metadata-only edit
exited 1 in CI for a value it was never asked for; on a TTY it stopped to
prompt, and the prompt rejects an empty entry, so there was no way to say
"leave the stored value alone" short of re-typing the secret. The read is
now skipped and the field omitted, which is what lets a metadata-only edit
run unattended. On a TTY, setting only a description no longer prompts.
Passing both spellings of the reveal flag is refused rather than silently
resolved.

Four mandatory hand-authored flags now say so, `billing logs` names its
key-type scope, and the dispatch list declares its columns.

* fix: close the gaps an adversarial review of this branch found

A conflict handler added earlier in this branch was dead code. It read the
Postgres error code off the thrown object, but the driver error arrives
wrapped with the real one on `cause`, so the check returned false on its
first line and the 409 never fired. Its test passed only because it threw a
flat shape production never produces. It now reads through the cause chain
with the shared helpers, compares the constraint name exactly instead of
matching a substring of the SQL, and its test throws the real wrapped error.

Resuming a conversation checked its workflow and its workspace but not its
type, so a conversation created by the web surface could be continued as a
CLI turn. It now refuses through the same uniform 404 as every other
mismatch, which closes the same omission on the web posting path. Minting
one no longer leaves a blank untitled row at the top of the Chat list.

The pre-write check on a minted API key refused fewer characters than the
writer does, so a key the check accepted could still fail at the write —
after the endpoint beside it was already stored, pairing a new endpoint with
the previous key. The two had drifted because the set was spelled three
times; there is now one.

A description claimed a processed count reported only the chunks that
changed. The update returns every row it matched, so re-enabling chunks that
were already enabled counts them all. Two OpenAPI sentences promised no
conflict detection and no persistence warnings in a dry run, both of which
the same branch had just made false. A described window was wrong whenever a
start was supplied without an end.

Listing the editors of a built-in skill answered a read with a modification
refusal on the internal surface. Archived table listings could reach the
strict folder projector again through a third scope value the input type
still allowed. A metadata-only secret write skipped the guard its
personal-scope twin has. The internal document boundary still took the two
processing fields as unbounded strings. Truncation was reported only from
the response envelope, so a clipped file body, row search and workflow-stats
list said nothing.

A staged download stopped watching for signals before it finished removing
its directory, and cleared every listener for the signal rather than its own.
Three tests asserted a contract constant against itself; they now drive
rendered help, real argv, or real render output.

* chore: regenerate the API reference, CLI surface, and CLI docs

The published reference still marked a secret value required and described
the delete parameter as one that also creates, the CLI surface still lacked
the marker that says which operations refuse a workspace key, and the
reference rendered an empty sentence for every repeatable filter whose
default is an empty list.

* test(cli): use the package's own delay helper in the staging poll

The audit bans a hand-rolled setTimeout promise. `sim-cli` does not depend
on the shared utils package, and its own idiom is `node:timers/promises`.

* fix: act on a second review round, and correct two earlier claims

The conflict pre-check read block ids from the wrong side. The writer
inserts each block's own `id` field while the check read the record key,
and the two can diverge because preparation copies a value under its key
without reconciling them. Edges already read the value and subflows are
genuinely keyed by the record key, so only blocks were wrong — collecting
every family from the values, as first suggested, would have broken
subflows instead.

A minted API key carrying leading or trailing whitespace passed the
pre-write check but failed the writer, leaving the new endpoint on disk
beside the previous key. It is refused up front now rather than trimmed: a
key is opaque, so trimming would store a value the server never issued and
turn a loud failure into an unexplained 401 later. The endpoint normalizer
does trim, which is what made a padded `--endpoint` fail only after the
browser flow had already minted a key.

A metadata-only secret write raced with deletion returned 500, because the
follow-up read that only assembles the response body threw an unclassified
error; it now reports the same not-found the non-racing miss already gave.
An unusable output format in the environment silently printed a table
instead of refusing. Two validation messages printed control characters
verbatim. A dry run now reports the preparation warnings its own commit
path returns.

The chat route created a titled conversation and never wrote a message, so
it appeared in the Chat list promising content it did not have. Both sides
of a successful turn are now persisted; a failed turn still writes nothing,
so a question is never stored without its answer.

Two claims of mine were wrong. The earlier commit message said `secrets
set` sent an empty value that overwrote the stored secret — it did not; the
prompt refuses off a TTY and rejects empty on one, so the old behaviour was
a clean refusal. And the delay helper commit said this package's idiom is
`node:timers/promises`; the package carries its own `sleep`, which is the
audit's sanctioned home and has five callers. It uses that now.

Also: a test asserting a deadlock stays unclassified could not fail, since
every candidate rejects it; it now pins a unique violation carrying no
constraint name. Workflow ids spelled with the file prefix are corrected in
the remaining fixtures, leaving the genuine file ids alone.

* fix: close a credential-misdirection path this branch had opened

Making the endpoint normalizer trim handled whitespace around a value but
not a control character inside one, and the URL parser removes those from
anywhere in its input — so a value that reads as one host could resolve to
another, and the profile's key went with it. The flag and environment paths
never touch the config writer, so its guard did not cover this. The
normalizer now refuses the same character set the writer does, which also
keeps the invariant that nothing it blesses can be refused by the write
that stores it. Comparing the parsed URL back against its input was the
alternative and is wrong: the parser rewrites percent-encoding, case,
internationalized hosts and default ports, so legitimate endpoints would be
refused.

The blank-query guard tested for exactly empty, so a whitespace-only value
still reached the wire — as a real zero on a numeric filter, an explicit
false on a boolean one, and as an encoded space the server then rejected.
It now refuses any value that is blank once trimmed, while a body string
keeps its meaning, an explicit zero still sends, and a value with content
around its whitespace is passed through untouched rather than trimmed.

A graph-id conflict reported 409 on the v2 route and fell through the older
persistence wrapper as an unclassified 500. That wrapper now classifies
orchestration failures through the cause chain, which also fixes a
pre-existing case where a workflow archived between authorization and the
locked read reported 500 rather than 404.

Persisting a chat turn claimed its row by id alone, so a conversation
soft-deleted mid-turn still received the messages and was bumped back up
the list. It now requires a live row. A turn whose caller hung up after the
model had already answered persisted nothing, though the work was done and
billed; it now persists and still reports the connection as closed.

An empty workspace id from the login response was read as no workspace at
all. A published description still promised a language-tag standard the
schema does not enforce.

The test asserting that a turn is stored before the final event drained the
whole response first, so it held whichever order the code used. It now
reads the stream incrementally and fails if the write moves after the
event.
2026-08-26 15:05:00 -07:00

1860 lines
81 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"openapi": "3.1.0",
"info": {
"title": "Sim API v2 — Logs",
"description": "Version 2 of the Sim REST API for workflow execution logs: listing and sorting runs with filters, retrieving complete diagnostic run snapshots, and reading bucketed execution statistics.",
"version": "2.0.0",
"contact": {
"name": "Sim Support",
"email": "help@sim.ai",
"url": "https://www.sim.ai"
},
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}
},
"servers": [
{
"url": "https://www.sim.ai",
"description": "Production"
}
],
"tags": [
{
"name": "Logs",
"description": "Query workflow execution logs and retrieve complete run diagnostics."
}
],
"security": [
{
"apiKey": []
}
],
"paths": {
"/api/v2/logs": {
"get": {
"operationId": "listLogs",
"summary": "List Logs",
"description": "List workflow execution logs for a workspace with filters, selectable detail, sorting by start time, duration, cost, or status, and opaque cursor pagination. Chat and Sim-agent job runs join the sequence with `includeJobRuns=true`, which is accepted only under `sortBy=startedAt` — their cost is stored as a document and their status is not comparable, so they cannot participate in the other orderings. Each item's `files` lists only the files the run itself produced, addressed by `downloadPath`; input attachments a caller supplied are read through the files API instead. Runs are hard-deleted once they pass the payer's log retention window, so an older run is simply absent rather than reported as removed. The window is 30 days from run start on the free plan, unbounded on Pro and Team, and set per organization on Enterprise with an optional per-workspace override. A workspace folder tree over 10,000 folders is a `413`.",
"tags": ["Logs"],
"parameters": [
{
"name": "workspaceId",
"in": "query",
"required": true,
"description": "Workspace whose execution logs should be returned.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"description": "Workspace whose execution logs should be returned."
}
},
{
"name": "workflowIds",
"in": "query",
"required": false,
"description": "Comma-separated workflow identifiers to include. An empty entry is rejected. At most 200 entries.",
"schema": {
"type": "string",
"description": "Comma-separated workflow identifiers to include. An empty entry is rejected. At most 200 entries."
}
},
{
"name": "triggers",
"in": "query",
"required": false,
"description": "Comma-separated trigger types to include. An empty entry is rejected. Values are matched exactly and are case-sensitive — every recorded trigger is lowercase, so `API` matches nothing while `api` matches. The vocabulary is open: it covers the core trigger types (`manual`, `api`, `schedule`, `chat`, `webhook`, `mcp`, `copilot`, `workflow`, `custom_block`) and the provider id of any webhook trigger (`slack`, `gmail`, `github`, …), so an unrecognized member is not rejected — it selects no runs. The literal value `all` is a sentinel that disables this filter entirely, so a list containing it returns runs of every trigger type; no real trigger type is named `all`. At most 100 entries.",
"schema": {
"type": "string",
"description": "Comma-separated trigger types to include. An empty entry is rejected. Values are matched exactly and are case-sensitive — every recorded trigger is lowercase, so `API` matches nothing while `api` matches. The vocabulary is open: it covers the core trigger types (`manual`, `api`, `schedule`, `chat`, `webhook`, `mcp`, `copilot`, `workflow`, `custom_block`) and the provider id of any webhook trigger (`slack`, `gmail`, `github`, …), so an unrecognized member is not rejected — it selects no runs. The literal value `all` is a sentinel that disables this filter entirely, so a list containing it returns runs of every trigger type; no real trigger type is named `all`. At most 100 entries."
}
},
{
"name": "level",
"in": "query",
"required": false,
"description": "Severity level to include.",
"schema": {
"type": "string",
"enum": ["info", "error"],
"description": "Severity level to include."
}
},
{
"name": "startDate",
"in": "query",
"required": false,
"description": "Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant.",
"schema": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant."
}
},
{
"name": "endDate",
"in": "query",
"required": false,
"description": "Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant.",
"schema": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant."
}
},
{
"name": "minDurationMs",
"in": "query",
"required": false,
"description": "Minimum total execution duration in milliseconds. Whole milliseconds from 0 to 2147483647; the stored duration is a 32-bit integer, so a fractional or out-of-range bound is rejected.",
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"description": "Minimum total execution duration in milliseconds. Whole milliseconds from 0 to 2147483647; the stored duration is a 32-bit integer, so a fractional or out-of-range bound is rejected."
}
},
{
"name": "maxDurationMs",
"in": "query",
"required": false,
"description": "Maximum total execution duration in milliseconds. Whole milliseconds from 0 to 2147483647; the stored duration is a 32-bit integer, so a fractional or out-of-range bound is rejected.",
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"description": "Maximum total execution duration in milliseconds. Whole milliseconds from 0 to 2147483647; the stored duration is a 32-bit integer, so a fractional or out-of-range bound is rejected."
}
},
{
"name": "minCost",
"in": "query",
"required": false,
"description": "Minimum execution cost in USD, from 0 to 1000000. A run is never charged a negative amount, so a negative bound is rejected rather than treated as a filter that matches every run.",
"schema": {
"type": "number",
"minimum": 0,
"maximum": 1000000,
"description": "Minimum execution cost in USD, from 0 to 1000000. A run is never charged a negative amount, so a negative bound is rejected rather than treated as a filter that matches every run."
}
},
{
"name": "maxCost",
"in": "query",
"required": false,
"description": "Maximum execution cost in USD, from 0 to 1000000. A run is never charged a negative amount, so a negative bound is rejected rather than treated as a filter that matches every run.",
"schema": {
"type": "number",
"minimum": 0,
"maximum": 1000000,
"description": "Maximum execution cost in USD, from 0 to 1000000. A run is never charged a negative amount, so a negative bound is rejected rather than treated as a filter that matches every run."
}
},
{
"name": "model",
"in": "query",
"required": false,
"description": "AI model used during execution.",
"schema": {
"type": "string",
"description": "AI model used during execution."
}
},
{
"name": "details",
"in": "query",
"required": false,
"description": "Response detail level. `full` adds the `workflow` summary to every workflow run; a job run never carries one, whatever this is set to. `includeTraceSpans=true` and `includeFinalOutput=true` each imply `full`, so either one adds `workflow` even when `details=basic` is sent explicitly.",
"schema": {
"default": "basic",
"type": "string",
"enum": ["basic", "full"],
"description": "Response detail level. `full` adds the `workflow` summary to every workflow run; a job run never carries one, whatever this is set to. `includeTraceSpans=true` and `includeFinalOutput=true` each imply `full`, so either one adds `workflow` even when `details=basic` is sent explicitly."
}
},
{
"name": "includeTraceSpans",
"in": "query",
"required": false,
"description": "Whether to include block-level trace spans. Implies `details=full`. Spans are pruned on their own retention schedule, so a run whose spans have aged out returns `traceSpans: []` rather than an error.",
"schema": {
"description": "Whether to include block-level trace spans. Implies `details=full`. Spans are pruned on their own retention schedule, so a run whose spans have aged out returns `traceSpans: []` rather than an error.",
"type": "boolean"
}
},
{
"name": "includeFinalOutput",
"in": "query",
"required": false,
"description": "Whether to include the final workflow output. Implies `details=full`, so the `workflow` summary is present regardless of what `details` is set to.",
"schema": {
"description": "Whether to include the final workflow output. Implies `details=full`, so the `workflow` summary is present regardless of what `details` is set to.",
"type": "boolean"
}
},
{
"name": "limit",
"in": "query",
"required": false,
"description": "Maximum log entries per page. Values outside 1–1000 are truncated and clamped into that range rather than rejected. Defaults to 100.",
"schema": {
"description": "Maximum log entries per page. Values outside 1–1000 are truncated and clamped into that range rather than rejected. Defaults to 100.",
"type": "integer",
"default": 100
}
},
{
"name": "cursor",
"in": "query",
"required": false,
"description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
"schema": {
"description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
"type": "string",
"minLength": 1
}
},
{
"name": "status",
"in": "query",
"required": false,
"description": "Comma-separated execution statuses to include, from `pending` | `running` | `paused` | `redacting` | `completed` | `failed` | `cancelled`. An empty entry is rejected. ANDed with `level`, which reports severity rather than lifecycle.",
"schema": {
"type": "string",
"description": "Comma-separated execution statuses to include, from `pending` | `running` | `paused` | `redacting` | `completed` | `failed` | `cancelled`. An empty entry is rejected. ANDed with `level`, which reports severity rather than lifecycle."
}
},
{
"name": "workflowName",
"in": "query",
"required": false,
"description": "Case-insensitive substring match against the run's workflow name. Runs whose workflow has been deleted match nothing, because the name is no longer joinable.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"description": "Case-insensitive substring match against the run's workflow name. Runs whose workflow has been deleted match nothing, because the name is no longer joinable."
}
},
{
"name": "includeJobRuns",
"in": "query",
"required": false,
"description": "Whether Chat and Sim-agent job runs join the sequence alongside workflow runs. Job runs report `kind: \"job\"`, carry no `workflow` summary, and never carry a cost ledger. They are dropped entirely — not partially matched — whenever a filter they cannot answer is set (`workflowIds`, `workflowName`, `folderPaths`, `model`, or `status`), so a filter never means two different things across the union. Accepted only under `sortBy=startedAt`: job runs record cost as a document and no comparable status, so they cannot participate in the other orderings.",
"schema": {
"description": "Whether Chat and Sim-agent job runs join the sequence alongside workflow runs. Job runs report `kind: \"job\"`, carry no `workflow` summary, and never carry a cost ledger. They are dropped entirely — not partially matched — whenever a filter they cannot answer is set (`workflowIds`, `workflowName`, `folderPaths`, `model`, or `status`), so a filter never means two different things across the union. Accepted only under `sortBy=startedAt`: job runs record cost as a document and no comparable status, so they cannot participate in the other orderings.",
"type": "boolean"
}
},
{
"name": "runId",
"in": "query",
"required": false,
"description": "Exact run identifier to match.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._:-]+$",
"description": "Exact run identifier to match."
}
},
{
"name": "sortBy",
"in": "query",
"required": false,
"description": "Field used to sort the result. `durationMs` and `cost` are null until a run settles; those runs order as though the value were below every recorded one, so they trail an ascending page and lead a descending one. Only `startedAt` can order Chat and Sim-agent job runs, so any other value is rejected together with `includeJobRuns=true`.",
"schema": {
"default": "startedAt",
"description": "Field used to sort the result. `durationMs` and `cost` are null until a run settles; those runs order as though the value were below every recorded one, so they trail an ascending page and lead a descending one. Only `startedAt` can order Chat and Sim-agent job runs, so any other value is rejected together with `includeJobRuns=true`.",
"type": "string",
"enum": ["startedAt", "durationMs", "cost", "status"]
}
},
{
"name": "sortOrder",
"in": "query",
"required": false,
"description": "Sort direction.",
"schema": {
"default": "desc",
"description": "Sort direction.",
"type": "string",
"enum": ["asc", "desc"]
}
},
{
"name": "folderPaths",
"in": "query",
"required": false,
"description": "Comma-separated workflow folder paths to include. At most 100 entries. A path covers its whole subtree, so `/prod` also selects runs in `/prod/nested`. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
"schema": {
"type": "string",
"description": "Comma-separated workflow folder paths to include. At most 100 entries. A path covers its whole subtree, so `/prod` also selects runs in `/prod/nested`. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error."
}
}
],
"responses": {
"200": {
"description": "A page of execution logs matching the filters.",
"headers": {
"X-RateLimit-Limit": {
"$ref": "#/components/headers/X-RateLimit-Limit"
},
"X-RateLimit-Remaining": {
"$ref": "#/components/headers/X-RateLimit-Remaining"
},
"X-RateLimit-Reset": {
"$ref": "#/components/headers/X-RateLimit-Reset"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2LogListResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"413": {
"$ref": "#/components/responses/PayloadTooLarge"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"503": {
"$ref": "#/components/responses/ServiceUnavailable"
}
}
}
},
"/api/v2/logs/{runId}": {
"get": {
"operationId": "getLog",
"summary": "Get Log",
"description": "Retrieve the diagnostic representation of a run, including its workflow snapshot, trace spans, final output, and cost. Trace spans are pruned on their own retention schedule, so an empty `traceSpans` array does not mean the run recorded none. A workspace folder tree over 10,000 folders is a `413`. Runs are hard-deleted once they pass the payer's log retention window, so an older run is simply absent rather than reported as removed. The window is 30 days from run start on the free plan, unbounded on Pro and Team, and set per organization on Enterprise with an optional per-workspace override.",
"tags": ["Logs"],
"parameters": [
{
"name": "runId",
"in": "path",
"required": true,
"description": "Unique workflow run identifier.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._:-]+$",
"description": "Unique workflow run identifier."
}
}
],
"responses": {
"200": {
"description": "The requested diagnostic log representation.",
"headers": {
"X-RateLimit-Limit": {
"$ref": "#/components/headers/X-RateLimit-Limit"
},
"X-RateLimit-Remaining": {
"$ref": "#/components/headers/X-RateLimit-Remaining"
},
"X-RateLimit-Reset": {
"$ref": "#/components/headers/X-RateLimit-Reset"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2LogDetailResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"413": {
"$ref": "#/components/responses/PayloadTooLarge"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"503": {
"$ref": "#/components/responses/ServiceUnavailable"
}
}
}
},
"/api/v2/logs/stats": {
"get": {
"operationId": "getLogStats",
"summary": "Get Log Statistics",
"description": "Bucketed run counts, success rate, error count, and mean latency for a workspace and for each of its workflows — the aggregate a caller would otherwise have to page every run to compute. The window spans `startDate` through `endDate` when both are supplied; an omitted edge falls back to the oldest matching run on the left and to the later of the newest matching run and now on the right. With no matching runs the right edge falls back to now and the left to 24 hours before that right edge — the trailing 24 hours when neither edge was supplied, and the 24 hours preceding `endDate` when only `endDate` was supplied. A supplied `startDate` is still used verbatim, so a `startDate` without an `endDate` yields `[startDate, now]`, which can be any width. The window is divided into exactly `segmentCount` equal buckets whose width is `max(60000, floor(windowMs / segmentCount))` milliseconds. The one-minute floor is a floor on bucket width, not on the window: when it applies, the series runs past `timeBounds.end` and the trailing buckets are empty rather than the window being compressed. A folder path covers its whole subtree. Per-workflow series are capped and `workflowsTruncated` reports whether the cap applied; the workspace totals are always computed from every workflow. Runs are hard-deleted once they pass the payer's log retention window, so an older run is simply absent rather than reported as removed. The window is 30 days from run start on the free plan, unbounded on Pro and Team, and set per organization on Enterprise with an optional per-workspace override. A workspace folder tree over 10,000 folders is a `413`.",
"tags": ["Logs"],
"parameters": [
{
"name": "workspaceId",
"in": "query",
"required": true,
"description": "Workspace whose execution statistics to summarize.",
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"description": "Workspace whose execution statistics to summarize."
}
},
{
"name": "workflowIds",
"in": "query",
"required": false,
"description": "Comma-separated workflow identifiers to include. At most 200 entries. An empty entry is rejected.",
"schema": {
"type": "string",
"description": "Comma-separated workflow identifiers to include. At most 200 entries. An empty entry is rejected."
}
},
{
"name": "folderPaths",
"in": "query",
"required": false,
"description": "Comma-separated workflow folder paths to include. At most 100 entries. A path covers its whole subtree. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
"schema": {
"type": "string",
"description": "Comma-separated workflow folder paths to include. At most 100 entries. A path covers its whole subtree. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error."
}
},
{
"name": "triggers",
"in": "query",
"required": false,
"description": "Comma-separated trigger types to include. An empty entry is rejected. The vocabulary is open, so an unrecognized member selects no runs; the literal `all` disables this filter.",
"schema": {
"type": "string",
"description": "Comma-separated trigger types to include. An empty entry is rejected. The vocabulary is open, so an unrecognized member selects no runs; the literal `all` disables this filter."
}
},
{
"name": "level",
"in": "query",
"required": false,
"description": "Severity level to include.",
"schema": {
"type": "string",
"enum": ["info", "error"],
"description": "Severity level to include."
}
},
{
"name": "startDate",
"in": "query",
"required": false,
"description": "Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant.",
"schema": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "Only include runs started at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant."
}
},
{
"name": "endDate",
"in": "query",
"required": false,
"description": "Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant.",
"schema": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "Only include runs started at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. A date without a time, or a timestamp carrying a UTC offset instead of `Z`, is rejected, as is year `0000`, which names no storable instant."
}
},
{
"name": "segmentCount",
"in": "query",
"required": false,
"description": "Number of equal time buckets to divide the window into, from 1 to 500. Exactly this many buckets are always returned. Buckets are never narrower than one minute, so on a short window the series extends past the end of the window rather than being compressed, and the trailing buckets are empty.",
"schema": {
"default": 72,
"description": "Number of equal time buckets to divide the window into, from 1 to 500. Exactly this many buckets are always returned. Buckets are never narrower than one minute, so on a short window the series extends past the end of the window rather than being compressed, and the trailing buckets are empty.",
"type": "integer",
"minimum": 1,
"maximum": 500
}
}
],
"responses": {
"200": {
"description": "Bucketed execution statistics for the workspace.",
"headers": {
"X-RateLimit-Limit": {
"$ref": "#/components/headers/X-RateLimit-Limit"
},
"X-RateLimit-Remaining": {
"$ref": "#/components/headers/X-RateLimit-Remaining"
},
"X-RateLimit-Reset": {
"$ref": "#/components/headers/X-RateLimit-Reset"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2LogStatsResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"413": {
"$ref": "#/components/responses/PayloadTooLarge"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"503": {
"$ref": "#/components/responses/ServiceUnavailable"
}
}
}
}
},
"components": {
"securitySchemes": {
"apiKey": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings, then API Keys. Operations that reject workspace keys say so in their own description."
}
},
"headers": {
"X-RateLimit-Limit": {
"description": "Maximum requests allowed in the current window.",
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"title": "Rate limit",
"description": "Maximum requests allowed in the current window."
}
},
"X-RateLimit-Remaining": {
"description": "Requests remaining in the current window.",
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"title": "Rate limit remaining",
"description": "Requests remaining in the current window."
}
},
"X-RateLimit-Reset": {
"description": "ISO 8601 timestamp when the current rate-limit window resets.",
"schema": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"title": "Rate limit reset",
"description": "ISO 8601 timestamp when the current rate-limit window resets."
}
},
"Retry-After": {
"description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset.",
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"title": "Retry after",
"description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset."
}
},
"X-Run-Id": {
"description": "Identifier assigned to the workflow run.",
"schema": {
"type": "string",
"minLength": 1,
"title": "Run identifier",
"description": "Identifier assigned to the workflow run."
}
}
},
"responses": {
"BadRequest": {
"description": "The request is invalid. This includes a query parameter sent with no value (`?limit=`, `?search=`), which is rejected rather than read as zero, empty, or the parameter default — omit the parameter instead.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
},
"example": {
"error": {
"code": "BAD_REQUEST",
"message": "Invalid request"
}
}
}
}
},
"Unauthorized": {
"description": "The API key is missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
},
"example": {
"error": {
"code": "UNAUTHORIZED",
"message": "API key required"
}
}
}
}
},
"Forbidden": {
"description": "The caller lacks the rights this operation requires. When the cause is one a caller can act on, `error.details.code` names it. A resource in a workspace the caller cannot reach at all answers `404` instead, so absence and denial are indistinguishable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
},
"example": {
"error": {
"code": "FORBIDDEN",
"message": "Insufficient workspace permissions",
"details": {
"code": "INSUFFICIENT_WORKSPACE_ROLE"
}
}
}
}
}
},
"NotFound": {
"description": "The requested resource was not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
},
"example": {
"error": {
"code": "NOT_FOUND",
"message": "Not found"
}
}
}
}
},
"PayloadTooLarge": {
"description": "The request, or a resource collection it must materialize, exceeds the allowed size: an oversized request body, a generated artifact past the download ceiling, or a workspace folder tree too large to load in full.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
},
"example": {
"error": {
"code": "PAYLOAD_TOO_LARGE",
"message": "Request body is too large"
}
}
}
}
},
"RateLimited": {
"description": "The caller exceeded the request rate limit.",
"headers": {
"Retry-After": {
"$ref": "#/components/headers/Retry-After"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
},
"example": {
"error": {
"code": "RATE_LIMITED",
"message": "API rate limit exceeded",
"details": {
"retryAfter": "2026-01-01T00:00:30.000Z"
}
}
}
}
}
},
"InternalError": {
"description": "An unexpected server error occurred.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
},
"example": {
"error": {
"code": "INTERNAL_ERROR",
"message": "Internal server error"
}
}
}
}
},
"ServiceUnavailable": {
"description": "A required service is temporarily unavailable. `Retry-After` carries the seconds to wait; treat it as a floor and add jitter. The header is omitted when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, because the run may already have started — reconcile against the returned run id instead of retrying.",
"headers": {
"Retry-After": {
"$ref": "#/components/headers/Retry-After"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
},
"example": {
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Service temporarily unavailable"
}
}
}
}
}
},
"schemas": {
"V2Error": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Stable machine-readable error code."
},
"message": {
"type": "string",
"description": "Human-readable explanation of the error."
},
"details": {
"description": "Structured error details. On a `403` whose cause a caller can act on, this carries a `code` from a closed set:\n- `INSUFFICIENT_WORKSPACE_ROLE` — The caller has access to the workspace but its role is below the one this operation requires.\n- `PERSONAL_API_KEYS_DISABLED` — The workspace's organization does not allow personal API keys. Use a workspace API key.\n- `WORKSPACE_KEY_OPERATION_NOT_PERMITTED` — This operation is not available to a workspace-scoped API key. Use a personal API key.\n- `PRINCIPAL_KIND_NOT_PERMITTED` — This operation does not accept the caller’s kind of API key.\n- `ORGANIZATION_MEMBERSHIP_REQUIRED` — The caller is not a member of the organization it named.\n- `ORGANIZATION_ADMIN_REQUIRED` — The caller is a member of the organization but not an admin or owner.\n- `ENTERPRISE_PLAN_REQUIRED` — The organization has no active enterprise subscription.\n- `ORGANIZATION_PLAN_REQUIRED` — The organization has no active organization subscription (Pro for Teams, Max for Teams, or Enterprise).\n- `AUDIT_LOGS_DISABLED` — Audit logging is not enabled for this deployment.\n- `SKILL_EDITOR_ACCESS_REQUIRED` — The caller can write in the workspace but is not an editor of this skill.\n- `SECRET_ADMIN_ACCESS_REQUIRED` — The caller can write in the workspace but is not an admin of this secret. Ask a workspace admin, or someone holding admin on the secret, to grant access or set the value.\n- `WORKSPACE_RESOURCE_LIMIT_REACHED` — The workspace already holds the maximum number of resources of this kind. Delete one, or contact Sim to raise the limit; the message names the ceiling.\n- `PUBLIC_SHARING_NOT_ALLOWED` — The workspace's organization does not permit sharing this resource publicly. An organization admin controls the policy.\n- `CREDENTIAL_ADMIN_ACCESS_REQUIRED` — The caller can reach the workspace but cannot administer this credential.\n- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\n- `WORKSPACE_PLAN_CAPABILITY_REQUIRED` — The workspace's plan does not include a capability this request depends on. The message names the capability; upgrading the workspace's plan is the remedy.\n- `CHAT_AUTH_MODE_NOT_PERMITTED` — The workspace's permission group does not allow the chat authentication mode the request selected. A mode already saved on the deployment may still be re-saved; changing to a disallowed one cannot.\n- `CONNECTOR_MANAGED_RESOURCE_READ_ONLY` — This resource is managed by a knowledge base connector and cannot be edited directly. Change it at the source and re-sync, or exclude the document from the connector."
}
},
"required": ["code", "message"],
"additionalProperties": false,
"description": "Canonical error details."
}
},
"required": ["error"],
"additionalProperties": false,
"title": "v2 error response",
"description": "Canonical error envelope returned by the public v2 API.",
"examples": [
{
"error": {
"code": "BAD_REQUEST",
"message": "The request is invalid."
}
}
]
},
"V2LogListItem": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": ["workflow", "job"],
"description": "Whether the run executed a workflow or a Chat / Sim-agent job. Job runs appear only when `includeJobRuns=true`."
},
"runId": {
"type": "string",
"description": "Unique run identifier."
},
"workflowId": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workflow identifier, or null when unavailable."
},
"deploymentVersionId": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Deployment version identifier, or null when unavailable."
},
"status": {
"type": "string",
"enum": [
"pending",
"running",
"paused",
"redacting",
"completed",
"failed",
"cancelled"
],
"description": "Current execution status, reported as persisted. `redacting` is transient while run output is scrubbed. `paused` is reported only when a resume attempt did not complete; a run held at a human-in-the-loop pause point reads `pending` here, and `paused` on the workflow run resources. Use those when the pause state matters."
},
"level": {
"type": "string",
"description": "Log severity level."
},
"trigger": {
"type": "string",
"description": "Trigger that started the run."
},
"startedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "ISO 8601 execution start timestamp."
},
"endedAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
},
{
"type": "null"
}
],
"description": "ISO 8601 execution end timestamp, or null while the run is active."
},
"totalDurationMs": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Total execution duration in milliseconds, or null while unavailable."
},
"cost": {
"anyOf": [
{
"type": "object",
"properties": {
"total": {
"type": "number",
"description": "Total execution cost in USD."
}
},
"required": ["total"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "Cost charged for the run, or null when the run has neither a recorded total nor an itemized ledger."
},
"files": {
"anyOf": [
{
"type": "array",
"items": {
"$ref": "#/components/schemas/V2LogFile"
}
},
{
"type": "null"
}
],
"description": "Files the run produced, or null when none are recorded. Only the run's own output files appear; input attachments a caller supplied are addressed through the files API instead."
},
"workflow": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workflow identifier, or null when unavailable."
},
"name": {
"type": "string",
"description": "Workflow name."
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workflow description, or null when unset."
},
"deleted": {
"type": "boolean",
"description": "Whether the workflow has been deleted."
}
},
"required": ["id", "name", "description", "deleted"],
"additionalProperties": false,
"description": "Workflow summary for a full-detail result."
},
"finalOutput": {
"description": "Final workflow output."
},
"traceSpans": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LogTraceSpan"
},
"description": "Block-level execution trace spans."
}
},
"required": [
"kind",
"runId",
"workflowId",
"deploymentVersionId",
"status",
"level",
"trigger",
"startedAt",
"endedAt",
"totalDurationMs",
"cost",
"files"
],
"additionalProperties": false,
"title": "Execution log summary",
"description": "Summary information for one workflow execution log."
},
"V2LogFile": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Identifier to address this file by on the download endpoint."
},
"name": {
"type": "string",
"description": "File name, including its extension."
},
"size": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "File size in bytes."
},
"type": {
"type": "string",
"description": "MIME type recorded for the file."
},
"downloadPath": {
"type": "string",
"description": "Path to fetch this file's bytes from, relative to the API host."
}
},
"required": ["id", "name", "size", "type", "downloadPath"],
"additionalProperties": false,
"title": "Execution log file",
"description": "A file produced by the run this log records."
},
"LogTraceSpan": {
"title": "Log trace span",
"description": "One recursive operation span in a workflow execution trace.",
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Trace-span identifier."
},
"name": {
"type": "string",
"description": "Trace-span name."
},
"type": {
"type": "string",
"description": "Trace-span category."
},
"duration": {
"type": "number",
"description": "Legacy span duration in milliseconds."
},
"durationMs": {
"type": "number",
"description": "Span duration in milliseconds."
},
"startTime": {
"type": "string",
"description": "ISO 8601 span start timestamp."
},
"endTime": {
"type": "string",
"description": "ISO 8601 span end timestamp."
},
"status": {
"type": "string",
"description": "Trace-span status."
},
"errorHandled": {
"type": "boolean",
"description": "Whether the recorded error was handled."
},
"errorType": {
"type": "string",
"description": "Recorded error type."
},
"errorMessage": {
"type": "string",
"description": "Recorded error message."
},
"blockId": {
"type": "string",
"description": "Workflow block associated with the span."
},
"input": {
"description": "Input captured for the traced operation."
},
"output": {
"description": "Output captured for the traced operation."
},
"tokens": {
"anyOf": [
{
"type": "number",
"description": "Total tokens attributed to the span."
},
{
"type": "object",
"properties": {
"total": {
"type": "number",
"description": "Total tokens."
},
"input": {
"type": "number",
"description": "Input tokens."
},
"output": {
"type": "number",
"description": "Output tokens."
}
},
"additionalProperties": false
}
],
"description": "Token usage attributed to the span."
},
"cost": {
"type": "object",
"properties": {
"total": {
"type": "number",
"description": "Total span cost in USD."
},
"input": {
"type": "number",
"description": "Input-token cost in USD."
},
"output": {
"type": "number",
"description": "Output-token cost in USD."
},
"toolCost": {
"type": "number",
"description": "Tool cost in USD."
}
},
"additionalProperties": false,
"description": "Cost attributed to the span."
},
"relativeStartMs": {
"type": "number",
"description": "Offset from the root span in milliseconds."
},
"toolCalls": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Tool-call identifier."
},
"name": {
"type": "string",
"description": "Invoked tool name."
},
"arguments": {
"description": "Arguments supplied to the tool call."
},
"result": {
"description": "Value returned by the tool call."
},
"error": {
"type": "string",
"description": "Tool-call error message."
},
"startTime": {
"type": "string",
"description": "ISO 8601 tool-call start timestamp."
},
"endTime": {
"type": "string",
"description": "ISO 8601 tool-call end timestamp."
},
"duration": {
"type": "number",
"description": "Tool-call duration in milliseconds."
}
},
"additionalProperties": {
"description": "Additional provider-specific tool-call metadata."
}
},
"description": "Tool calls recorded by the span."
},
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LogTraceSpan"
},
"description": "Nested child trace spans."
}
},
"required": ["id", "name", "type"],
"additionalProperties": {
"description": "Additional provider-specific trace-span metadata."
}
},
"V2LogListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/V2LogListItem"
},
"description": "Items in the current page."
},
"nextCursor": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
}
},
"required": ["data", "nextCursor"],
"additionalProperties": false,
"title": "Log list response",
"description": "A cursor-paginated page of workflow execution logs.",
"examples": [
{
"data": [
{
"kind": "workflow",
"runId": "e4f8d2b6-9a1c-4e3d-8b7f-5c0a2d9e6f13",
"workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
"deploymentVersionId": "dep_2c4e6a8b0d1f",
"status": "completed",
"level": "info",
"trigger": "api",
"startedAt": "2026-01-15T10:30:00.000Z",
"endedAt": "2026-01-15T10:30:01.250Z",
"totalDurationMs": 1250,
"cost": {
"total": 0.0032
},
"files": [
{
"id": "f1c3a7d0-4b52-4a8e-9f61-2d7c8b3e5a04",
"name": "summary.pdf",
"size": 18422,
"type": "application/pdf",
"downloadPath": "/api/v2/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/runs/e4f8d2b6-9a1c-4e3d-8b7f-5c0a2d9e6f13/files/f1c3a7d0-4b52-4a8e-9f61-2d7c8b3e5a04"
}
]
}
],
"nextCursor": "eyJzdGFydGVkQXQiOiIyMDI2LTAxLTE1VDEwOjMwOjAwMFoifQ=="
}
]
},
"V2LogDetail": {
"type": "object",
"properties": {
"runId": {
"type": "string",
"description": "Unique run identifier."
},
"workflowId": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workflow identifier, or null when unavailable."
},
"deploymentVersionId": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Deployment version identifier, or null when unavailable."
},
"status": {
"type": "string",
"enum": [
"pending",
"running",
"paused",
"redacting",
"completed",
"failed",
"cancelled"
],
"description": "Current execution status, reported as persisted. `redacting` is transient while run output is scrubbed. `paused` is reported only when a resume attempt did not complete; a run held at a human-in-the-loop pause point reads `pending` here, and `paused` on the workflow run resources. Use those when the pause state matters."
},
"level": {
"type": "string",
"description": "Log severity level."
},
"trigger": {
"type": "string",
"description": "Trigger that started the run."
},
"startedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "ISO 8601 execution start timestamp."
},
"endedAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
},
{
"type": "null"
}
],
"description": "ISO 8601 execution end timestamp, or null while the run is active."
},
"totalDurationMs": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Total execution duration in milliseconds, or null while unavailable."
},
"files": {
"anyOf": [
{
"type": "array",
"items": {
"$ref": "#/components/schemas/V2LogFile"
}
},
{
"type": "null"
}
],
"description": "Files the run produced, or null when none are recorded. Only the run's own output files appear; input attachments a caller supplied are addressed through the files API instead."
},
"workflow": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workflow identifier, or null when unavailable."
},
"name": {
"type": "string",
"description": "Workflow name."
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workflow description, or null when unset."
},
"folderPath": {
"anyOf": [
{
"type": "string",
"title": "Folder path",
"description": "Canonical slash-prefixed folder path. `/` is the workspace root. Segments are percent-encoded, so a folder shown as \"New folder\" is `/New%20folder`: everything outside `A-Z a-z 0-9 - _ . ~` is escaped as uppercase hex, and only that exact encoding is accepted. A trailing slash, an empty segment, and a literal `.` or `..` segment are rejected. At most 64 segments and 4096 encoded bytes.",
"maxLength": 4096
},
{
"type": "null"
}
],
"description": "Canonical folder path of the workflow, in the same form `folderPaths` accepts as a filter: `/` for a workflow at the workspace root. Null only when the path cannot be resolved — the folder has been deleted, or the workflow itself no longer exists."
},
"ownerEmail": {
"anyOf": [
{
"type": "string",
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
{
"type": "null"
}
],
"description": "Workflow owner email, or null when unavailable."
},
"workspaceId": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Owning workspace identifier, or null when unavailable."
},
"createdAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
},
{
"type": "null"
}
],
"description": "ISO 8601 workflow creation timestamp, or null when unavailable."
},
"updatedAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
},
{
"type": "null"
}
],
"description": "ISO 8601 workflow update timestamp, or null when unavailable."
},
"deleted": {
"type": "boolean",
"description": "Whether the workflow has been deleted."
}
},
"required": [
"id",
"name",
"description",
"folderPath",
"ownerEmail",
"workspaceId",
"createdAt",
"updatedAt",
"deleted"
],
"additionalProperties": false,
"description": "Workflow snapshot associated with the execution."
},
"workflowState": {
"anyOf": [
{
"type": "object",
"properties": {},
"additionalProperties": {
"description": "One top-level snapshot section — `blocks`, `edges`, `loops`, `parallels`, or `variables` — passed through as stored."
}
},
{
"type": "null"
}
],
"description": "Workflow graph snapshot captured for the run, or null when none is retained. Credential-bearing values are redacted to null: `oauth-input`, `password: true`, table sub-block values, sensitive nested tool parameters, and any parameter without authoritative codec metadata. `{{VAR}}` references in non-opaque fields are preserved."
},
"traceSpans": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LogTraceSpan"
},
"description": "Materialized block-level execution trace spans."
},
"finalOutput": {
"anyOf": [
{
"description": "Materialized final workflow output value."
},
{
"type": "null"
}
],
"description": "Materialized final workflow output, or null when none was produced."
},
"cost": {
"anyOf": [
{
"type": "object",
"properties": {
"total": {
"type": "number",
"description": "Total execution cost in USD."
},
"items": {
"anyOf": [
{
"type": "array",
"items": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["fixed", "model", "tool"],
"description": "What the line is for: the run's base fee (`fixed`), one model's inference (`model`), or one metered tool or integration call (`tool`)."
},
"description": {
"type": "string",
"description": "Human-readable name of the billed item, such as the model or tool id."
},
"cost": {
"type": "number",
"description": "Amount billed for this line, in USD."
},
"inputTokens": {
"description": "Input tokens attributed to this line. Absent for lines that do not bill tokens.",
"type": "number"
},
"outputTokens": {
"description": "Output tokens attributed to this line. Absent for lines that do not bill tokens.",
"type": "number"
}
},
"required": ["category", "description", "cost"],
"additionalProperties": false,
"description": "One billed line of a run, folded across every event that billed it."
}
},
{
"type": "null"
}
],
"description": "Billed lines reconciling to `total`, or null when no itemized ledger exists for the run."
}
},
"required": ["total", "items"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "Cost charged for the run, or null when unavailable."
},
"workflowInput": {
"anyOf": [
{
"description": "Caller-supplied trigger payload for the run."
},
{
"type": "null"
}
],
"description": "Input the run was triggered with, or null when the run recorded none. Credential-bearing and PII-masked values are redacted the same way `finalOutput` is."
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "ISO 8601 log creation timestamp."
}
},
"required": [
"runId",
"workflowId",
"deploymentVersionId",
"status",
"level",
"trigger",
"startedAt",
"endedAt",
"totalDurationMs",
"files",
"workflow",
"workflowState",
"traceSpans",
"finalOutput",
"cost",
"workflowInput",
"createdAt"
],
"additionalProperties": false,
"title": "Execution log detail",
"description": "Detailed workflow execution log including state, trace, output, and cost."
},
"V2LogDetailResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
"$ref": "#/components/schemas/V2LogDetail"
}
},
"required": ["data"],
"additionalProperties": false,
"title": "Log detail response",
"description": "The complete diagnostic representation of a workflow run.",
"examples": [
{
"data": {
"runId": "e4f8d2b6-9a1c-4e3d-8b7f-5c0a2d9e6f13",
"workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
"deploymentVersionId": "dep_2c4e6a8b0d1f",
"status": "completed",
"level": "info",
"trigger": "api",
"startedAt": "2026-01-15T10:30:00.000Z",
"endedAt": "2026-01-15T10:30:01.250Z",
"totalDurationMs": 1250,
"files": null,
"workflow": {
"id": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
"name": "Customer Support Agent",
"description": "Routes incoming support tickets and drafts responses",
"folderPath": "/",
"ownerEmail": "jane@example.com",
"workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
"createdAt": "2025-01-10T09:00:00.000Z",
"updatedAt": "2025-06-18T16:45:00.000Z",
"deleted": false
},
"workflowState": {
"blocks": {},
"edges": []
},
"traceSpans": [],
"finalOutput": {
"result": "Hello, world!"
},
"cost": {
"total": 0.0032,
"items": [
{
"category": "fixed",
"description": "Base execution charge",
"cost": 0.001
},
{
"category": "model",
"description": "gpt-5",
"cost": 0.0022,
"inputTokens": 1840,
"outputTokens": 260
}
]
},
"workflowInput": {
"ticketId": "T-4821"
},
"createdAt": "2026-01-15T10:30:00.000Z"
}
}
]
},
"V2WorkflowLogStats": {
"type": "object",
"properties": {
"workflowId": {
"type": "string",
"description": "Workflow identifier, or the literal `deleted` for the single series that collects runs whose workflow no longer exists."
},
"workflowName": {
"type": "string",
"description": "Workflow name, or `Deleted Workflow`."
},
"segments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/V2LogStatsSegment"
},
"description": "One entry per bucket, in order, including buckets with no runs."
},
"totalExecutions": {
"type": "number",
"description": "Runs for this workflow across the window."
},
"totalSuccessful": {
"type": "number",
"description": "Runs for this workflow that did not error."
},
"overallSuccessRate": {
"type": "number",
"description": "Percentage of runs that did not error, from 0 to 100. 100 when there were no runs."
}
},
"required": [
"workflowId",
"workflowName",
"segments",
"totalExecutions",
"totalSuccessful",
"overallSuccessRate"
],
"additionalProperties": false,
"title": "Per-workflow log stats",
"description": "Bucketed run counts and success rate for one workflow."
},
"V2LogStatsSegment": {
"type": "object",
"properties": {
"timestamp": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "ISO 8601 start of the bucket."
},
"totalExecutions": {
"type": "number",
"description": "Runs that started inside the bucket."
},
"successfulExecutions": {
"type": "number",
"description": "Runs in the bucket that did not error."
},
"avgDurationMs": {
"type": "number",
"description": "Mean duration of the bucket's runs in milliseconds, weighted by run count. Zero when no run in the bucket recorded a duration."
}
},
"required": ["timestamp", "totalExecutions", "successfulExecutions", "avgDurationMs"],
"additionalProperties": false,
"title": "Log stats bucket",
"description": "Run counts and mean latency for one time bucket."
},
"V2LogStats": {
"type": "object",
"properties": {
"workflows": {
"type": "array",
"items": {
"$ref": "#/components/schemas/V2WorkflowLogStats"
},
"description": "Per-workflow series, ordered by error rate descending then by name, capped at 200 entries."
},
"workflowsTruncated": {
"type": "boolean",
"description": "Whether `workflows` was cut to 200 entries. The workspace totals and `aggregateSegments` are computed from every workflow before the cut, so they stay exact either way."
},
"aggregateSegments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/V2LogStatsSegment"
},
"description": "Workspace-wide totals per bucket, in the same order as each workflow series."
},
"totalRuns": {
"type": "number",
"description": "Runs in the window across the whole workspace."
},
"totalErrors": {
"type": "number",
"description": "Runs in the window that errored."
},
"avgLatency": {
"type": "number",
"description": "Mean run duration in milliseconds across the window, weighted by run count."
},
"timeBounds": {
"type": "object",
"properties": {
"start": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "ISO 8601 start of the window."
},
"end": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "ISO 8601 end of the window."
}
},
"required": ["start", "end"],
"additionalProperties": false,
"description": "The window the buckets span. `startDate` and `endDate` are used verbatim when supplied; an omitted edge falls back to the oldest matching run on the left and to the later of the newest matching run and now on the right. With no matching runs the right edge falls back to now and the left to 24 hours before that right edge — the trailing 24 hours when neither edge was supplied, and the 24 hours preceding `endDate` when only `endDate` was supplied. A supplied `startDate` is still used verbatim, so a `startDate` without an `endDate` yields `[startDate, now]`, which can be any width."
},
"segmentMs": {
"type": "number",
"description": "Width of one bucket in milliseconds."
}
},
"required": [
"workflows",
"workflowsTruncated",
"aggregateSegments",
"totalRuns",
"totalErrors",
"avgLatency",
"timeBounds",
"segmentMs"
],
"additionalProperties": false,
"title": "Execution log statistics",
"description": "Bucketed success rate, error count, and latency for a workspace and each of its workflows."
},
"V2LogStatsResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
"$ref": "#/components/schemas/V2LogStats"
}
},
"required": ["data"],
"additionalProperties": false,
"title": "Log statistics response",
"description": "Bucketed success rate, error count, and latency for a workspace and its workflows.",
"examples": [
{
"data": {
"workflows": [
{
"workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
"workflowName": "Customer Support Agent",
"segments": [
{
"timestamp": "2026-01-15T10:00:00.000Z",
"totalExecutions": 40,
"successfulExecutions": 38,
"avgDurationMs": 1180
}
],
"totalExecutions": 40,
"totalSuccessful": 38,
"overallSuccessRate": 95
}
],
"workflowsTruncated": false,
"aggregateSegments": [
{
"timestamp": "2026-01-15T10:00:00.000Z",
"totalExecutions": 40,
"successfulExecutions": 38,
"avgDurationMs": 1180
}
],
"totalRuns": 40,
"totalErrors": 2,
"avgLatency": 1180,
"timeBounds": {
"start": "2026-01-15T10:00:00.000Z",
"end": "2026-01-15T22:00:00.000Z"
},
"segmentMs": 600000
}
}
]
}
}
},
"x-generated-by": "scripts/generate-openapi.ts"
}