mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(v2): close the correctness gaps an end-to-end audit found (#6655)
* fix(v2): stop a third-party tool description from 500ing MCP discovery
`v2McpToolInputSchema` declared `description: z.string().optional()` inside a
`.catchall(z.unknown())` object, and a declared key beats the catchall. The MCP
SDK's own `ToolSchema.inputSchema` does not declare `description` at all, so any
value — including the JSON `null` a Python server emits for an absent one —
passes its validation and reaches Sim unchecked. The builder's outbound `.parse()`
then threw, and the discovery error policy correctly declines to classify a
Sim-side schema defect, so the endpoint that completes MCP onboarding answered a
bare 500. The key is dropped and left to the catchall; `type`, `properties`, and
`required` stay pinned because the SDK enforces those at least as tightly.
Also in the v2 resources family:
- The single-resource query schemas for MCP servers, skills, custom tools, and
secrets are now `.strict()`, matching every list in the same family. A mistyped
flag was silently ignored behind a 200.
- `openapi/resources.ts` re-derived `RESOURCE_ERRORS` and
`RESOURCE_CONFLICT_ERRORS` inline in 21 of 22 operations. They now import the
shared constants; the generated spec is unchanged, which is the point.
- The internal MCP refresh route stamped `updatedAt` alongside `lastToolsRefresh`.
`updatedAt` means "configuration last changed" and is a public keyset sort, so
a refresh moved rows out from under an in-flight page. `updateServerStatus`
already held that invariant; the route now matches it.
- The discovery cooldown is a typed `McpServerCooldownError` rather than a
substring search for `cooldown`. `McpConnectionError` interpolates the server's
display name into its message, so a server named after the word was reported as
a transient cooldown when its connection had genuinely failed.
* fix(v2): close correctness gaps in the workflows deployment surface
Deploy and rollback bodies were plain objects, so a misspelled key was
stripped rather than rejected. On rollback that is silent misbehavior:
an omitted `version` legitimately means "reactivate the preceding
version", so `{"versoin": 5}` rolled back somewhere else and answered
200. Both v2 bodies, the run-read query, and the versions cursor are now
strict.
Deployment versions are an `integer` column, but the path param, the
versions cursor, and the v1 body each bounded it differently or not at
all — an out-of-range value overflowed the comparison into an
unclassifiable 500. One exported bound now covers all three.
Resume admission raised bare `Error`s for a stale contextId or an
already-resumed run, which the resume surfaces could not classify and
reported as 500. They now use the sibling `ResumeAdmissionError` already
in that file, carrying 404/409/400 and whether an automatic retry can
clear the refusal.
Docs corrections: rollback publishes the 409 its webhook-path conflict
already produces; deploy/undeploy/rollback reject a workspace key with
403, not the concealed 404 they documented; the workflows OpenAPI module
imports the shared error sets instead of re-deriving them; import and
the folder ops explain their folder-tree 413. The export route is marked
`headSafe: false` so a HEAD probe stops filing a WORKFLOW_EXPORTED audit
event for an export that never happened. `runId` is one bounded schema
across the run and log resources.
* fix(v2): conceal knowledge upload existence, tighten knowledge/files bounds
Security: the four knowledge document-upload routes rendered a bare upload
error policy with no resource concealment, while every sibling knowledge route
uses one. Because the use case resolves the knowledge-base context before
workspace authorization, the unconcealed 403 told any valid API-key holder that
a knowledge base exists in a workspace it cannot reach — the exact signal
GET /api/v2/knowledge/{id} withholds by answering 404 either way. All four now
use the composed concealing policy, which also renders the 415/402/413 the
route-local renderer already handled; that duplicate renderer is deleted.
Contracts:
- POST /knowledge/search is strict. It was the only non-strict v2 request body,
so a mis-cased rerankerEnabled or topK returned 200 with the key stripped,
changing what the caller was billed and silently disabling reranking.
- The document list takes limit, cursor, and search from the shared v2 schemas.
search was an unbounded, empty-accepting v1 string, so ?search= answered 200
with a full page here and 400 on GET /knowledge, and the term reached an
unindexed filename LIKE scan with no ceiling.
- The 16 non-strict single-field workspace query slices across both families are
strict, matching GET /knowledge/{id}/tags.
- GET /audit-logs takes workspaceIdSchema instead of a bare string (?workspaceId=
was forwarded as a filter and returned zero rows) and the shared run-window
bounds for startDate/endDate.
Documentation:
- listAuditLogs drops the 404 it has no code path to emit.
- upsertFileShare describes its workspace-key refusal as the 403 it renders;
the operation denies the key by principal kind, which the concealment policy
does not rewrite.
- The 12 body-reading knowledge and files operations publish the 413 their
pre-validation body read raises, and the file list publishes the folder-tree
413 its now-capped path index raises.
Correctness: queryWorkspaceFilePage loads its folder path index under
MAX_FOLDERS_PER_WORKSPACE like the workflow, table, and knowledge lists. An
uncapped index does not fail on truncation, so a real folder outside the read
rows resolved to undefined and answered "Folder not found".
* fix(v2): publish the reachable 413 on body-carrying resources ops
`parseRequest` buffers a JSON body through `parseJsonBody` under
`DEFAULT_MAX_JSON_BODY_BYTES` before any schema runs, and the v2 builders supply
`V2_PARSE_DEFAULTS.payloadTooLargeResponse`, so every operation whose contract
declares a body already answers 413 above the cap. The resources family
published it on none of them. A status a caller cannot see in the spec is a
status they will not handle.
Adds `RESOURCE_BODY_ERRORS` and `RESOURCE_CONFLICT_BODY_ERRORS` to the shared
sets and applies them to the seven affected operations: createMcpServer,
updateMcpServer, createSkill, updateSkill, createCustomTool, updateCustomTool,
and setSecret. All seven are `defineV2JsonRoute` handlers on non-GET methods
with no `parseOptions` override, so the 413 is genuinely reachable on each. The
new sets are opt-in rather than folded into the base sets precisely because
reachability is not automatic — an operation with no body, or one whose payload
reaches it through an uncapped path, would be publishing a response that can
never arrive.
A sweep test pins the invariant across the resources, billing, and logs
documents. It is one-directional by construction: several bodyless operations
publish 413 for their own folder-tree and render ceilings, so the converse would
flag correct documentation.
Also completes the shared-constant consolidation started in cd3efefab9:
`openapi/billing.ts` and `openapi/logs.ts` each re-derived `RESOURCE_ERRORS`
inline in two operations. Both now import it, and both regenerate byte-identical.
* fix(v2): head-safe binary downloads, coded 403s, and truthful surface docs
Adds `headSafe` to `defineV2BinaryRoute`, mirroring the JSON builder: a HEAD
on a route that declares itself unsafe is authenticated and rate-limited, then
answered bodiless before parsing or executing. `GET /api/v2/files/{fileId}` is
the one binary v2 route and it records a `FILE_DOWNLOADED` audit event, so a
HEAD probe used to fabricate a download that never happened.
Names the cause of five refusals that reached the wire as codeless 403s
(billing principal-kind, personal-keys-disabled and role, secret admin and
write, the workspace table quota, and public sharing), adding three members to
the closed `FORBIDDEN_DETAIL_CODES` set. The billing cross-tenant refusal is
concealed as a 404 instead of coded, and the credential-list and knowledge
file-ownership refusals stay codeless deliberately, documented at the site.
Makes `WORKSPACE_KEY_OPERATION_NOT_PERMITTED` reachable: an operation that
denies workspace keys also omits them from `principalKinds`, so the kind guard
always fired first and callers got `PRINCIPAL_KIND_NOT_PERMITTED` instead of
the published code.
Drops the unused 410 response, shares one `order` schema between the two run
reads so both specs spell the enum the same way, and corrects the false
statements about 403 codes, 413 causes, cursor schemes, and full-set lists in
the conventions skill and the contract TSDoc.
* fix(tables): close the v2 tables correctness and contract gaps
- updateColumnOptions was the only column mutator with no lock assert: an
options-only PATCH applied on a schema-locked table, and an option REMOVAL
cleared cells on a delete-locked one. Assert schema always, escalate to the
destructive gate only when options are dropped.
- GET/DELETE /tables/imports/{id} 500'd on a first-party import job (null
payload) or an unrepresentable status. Both now read as absent, so the answer
is the 404 it always was.
- Offset cursors stamped the sort but not the filters, so a page-2 cursor
replayed under a different predicate paged an unrelated sequence silently.
Offsets now carry a filter fingerprint and refuse a mismatch.
- Publish 413 on every tables operation that accepts a request body: the v2
JSON builder reads the body under a byte ceiling before validation, so the
status is reachable on all of them. Derived at document assembly so a new
route cannot regress it.
- Enforce MAX_VIEWS_PER_TABLE on view create, making the list contract's
"small bounded set" claim true.
- Accept the upload control token on the import read, so an upload-backed
import is readable during the phase its own 201 reported; drop the `queued`
status the reads can never return.
- Declare the Find search-term cap, the Find match cap, and the run row-id
ceiling the domain already enforces.
- Uniform 201 on the row and column creates.
* docs(v2): record why the two migrate-on-read GETs stay head-safe
An enumeration of side-effecting v2 GETs flagged these two for issuing a
workflow_blocks update. The write is convergent and would be issued by the
next ordinary read, and headSafe: false answers 200 unconditionally, so
declaring it would cost HEAD its existence check to prevent nothing.
* fix(api): classify the caller input that reached the driver unvalidated
Four families of caller-reachable 500s share one shape: a value the
contract admits, the application forwards, and the database rejects.
An unclassified driver throw renders as INTERNAL_ERROR, so a bad
request came back as a server fault — on pure reads as well as writes.
NUL bytes are rejected at the contract boundary, in parseRequest, not
per field. A shared string primitive only protects the fields somebody
remembers to build on it, and it cannot protect the values that have no
string schema at all: a table cell and a predicate value are z.unknown()
because their type belongs to the column, not the wire, and those are
exactly the values found reaching the driver. One scan over the already
validated params/query/body covers every field including the ones nobody
has enumerated. Only U+0000 is rejected; every other control character
is ordinary content that Postgres stores verbatim.
Date bounds on a filter are now parsed, not merely type-checked, with
the same normalizer the date column type uses to store cells — so the
filter grammar and the storage grammar agree, and gt/gte/lt/lte on both
JSONB date columns and the createdAt/updatedAt system columns answer an
unparseable bound with 400 instead of an invalid-input-syntax 500.
An afterRowId/beforeRowId anchor that does not exist is a classified
not-found rather than a bare Error, and a zero-byte knowledge document
is refused at admission: every parser rejects an empty buffer outright,
so the upload could only ever consume storage and quota on its way to
processingStatus failed.
* fix(v2): stop six endpoints from returning a confident untruth
Six defects that share a shape: a 200 that misrepresents what happened,
which is the one class a caller cannot detect from the response.
Knowledge search silently degraded. Reranking is implemented and does
run, but a deployment with no Cohere credential, a provider error, or a
timeout was swallowed into a warning log and answered 200 with plain
vector ordering and no `rerankerScore` anywhere — indistinguishable from
a reranker that ran and agreed with the vector order. The fallback stays
(an outage should not take search down) and is now reported:
`rerankerStatus` is required on every search response. v2 also omitted
the `rerankerModel` default the internal contract supplies, so
`rerankerEnabled: true` alone failed the use case's model guard and
returned unreranked results after paying for the widened candidate
retrieval; it now defaults like its sibling.
`GET /billing/logs` accepted `startDate`/`endDate` with any relative
period and dropped them, answering over the default 30-day window — a
caller reconciling charges got real rows that were not the rows it asked
for. Both bounds are now rejected outside `period=custom`, take the same
strict UTC form as `GET /logs` via the shared `v2RunWindowBoundSchema`,
and reject an inverted window instead of returning an empty page.
MCP registration stamped `connectionStatus: 'connected'` and
`lastConnected: now` at insert without contacting the endpoint, and did
the same on any non-OAuth re-registration while leaving `lastError`
stale. `tool-validation` gates tool availability on that column, so an
unreachable server read as healthy. Both paths now leave the columns at
their honest defaults for `mcpService.updateServerStatus` to move after
a real discovery; the client-side optimistic copy matches.
`skills.create` allowed a workspace API key while every other skill
write denies one, so a key could only ever accumulate skills it could
never remove — and the row it left was attributed to the workspace's
billing owner, minting an editor grant for a human who did not act.
Creation now denies a workspace key, making the lifecycle symmetric on
the per-skill editor model that authorizes the rest of it.
`runCount` counts successful non-paused runs and is never decremented by
retention, so it disagrees with the runs list in both directions; the
description now says so rather than claiming "total recorded runs". Run
retention itself was undocumented — free-plan runs are hard-deleted after
30 days, which is why a workflow reports runs beside an empty list — and
is now stated on both reads over the execution-log table.
* fix(tables): refuse the writes v2 was silently discarding
- Uncoercible cell values were stored as null under a 200 on any optional
column: "abc"/true/[1] into number, "yes"/1/{} into boolean, "not-a-date"
into date, an undeclared option into select, an object into string. The
read side already 400s on the same mismatch in a predicate, so the two
halves of the API disagreed about the same value. `coerceRowValues` /
`coerceRowToSchema` now take an explicit policy and default to `reject`;
`null` is passed only where a machine produced the value for a cell no
caller typed — a computed (workflow/enrichment) write and a CSV import,
neither of which has anyone to answer with a 400.
- A multi-select coerced `["green"]` to `[]` — the drop was inside the
registry, so no policy above it could see it. It now refuses any part that
matches no option, which is what the single branch and the bulk retype gate
already did.
- A bare number in a date cell was read as epoch milliseconds, so the far more
common Unix-seconds shape stored a timestamp 50 years early. The unit is not
recoverable from the value and both readings are in range, so a bare number
is refused in both directions and the retype gate no longer needs an
override to be stricter than the write path.
- Unknown column names were dropped by the name→id remap: an insert of
{"nosuchcol":"x"} created an empty row under a 201, and a patch of
{"zzz":"x"} answered updatedCount:0, indistinguishable from an empty match.
The v2 row boundary now names them and refuses.
- The table ceiling was enforced only inside createTable, which for an
upload-backed import does not run until the CSV has crossed the wire: a full
workspace got a 201 and a presigned PUT for up to 5 GiB, then a 403 at
complete with an orphaned object left behind. The advisory check now runs
when the session is created; the authoritative one stays in the transaction
because the quota can move mid-upload.
- Cap workflow groups per table. GET /tables/{id}/groups is published as a
full-set list, and the group count had no bound of its own — the indirect
one does not survive an update path that adds no columns.
- Present a group's outputs/dependencies/inputMappings by column NAME. They
are created by name, stored by id, and were read back as ids on a surface
that is otherwise name-keyed, so a group could not be round-tripped.
- Publish the predicate grammar: the operator set, the per-type restrictions,
and that `*` — not `%` — is the wildcard. It was true only in the SQL
builder's own comments, so the natural guess matched zero rows under a 200.
- Stop advertising a `workflowId` default of "" on group create; a manual
group that omits it has always been refused.
* fix(v2): bind every paged list's cursor to its filters, not just its sort
A v2 cursor names a position in one sequence, and a list decides that
sequence from its sort AND its filters. Only the sort was stamped on the
shared keyset codec, so a cursor from an unfiltered walk was accepted
under a changed `search`, `scope`, `deployedOnly`, or folder and answered
from a sequence the caller never asked for. The two offset lists already
stamped both; nothing else did.
The failure differs by scheme but is silent in both. An offset lands at
an unrelated ordinal. A keyset stays internally coherent — correctly
ordered, duplicate-free — and drops every match sorting before its
position, which a caller holding an opaque token reads as "almost
nothing matched".
One mechanism, shared with the table-row codec: canonical JSON plus a
SHA-256 fingerprint (`lib/api/cursor-binding.ts`), stamped by
`cursorFilterScope` alongside `cursorSortKey`. The two stamps stay
separate so the 400 names which half changed. `limit` is never bound —
it selects how much of the sequence to return, not what it is.
The three lists whose token is minted by a domain codec (`/logs`,
`/audit-logs`, `/billing/logs`) get the same binding by wrapping that
token in a query-stamped envelope; the domain cursor is untouched.
`present` now also receives the parsed request, so a presenter reads the
filters it stamps straight from the query instead of the use case
carrying an HTTP cursor concern back out — the `cursorSort`/`cursorScope`
round-trips through three application services are removed.
`list-pagination.test.ts` now declares each paged list's binding and
checks it against the contract in both directions, so a new list, or a
new filter on an existing one, fails until its binding is decided.
* fix(v2): authorize HEAD probes and declare every v2 query schema
Two ways the v2 surface answered a request it had not checked.
`headSafe: false` exists so a HEAD cannot fire the side effect its GET
performs — an outbound MCP discovery, a FILE_DOWNLOADED audit event, a
WORKFLOW_EXPORTED audit event. The short-circuit sat between admission
and parsing, so it returned a bodiless 200 before resource authorization
ran at all: authorization lives inside the use case, and the use case was
exactly what the short-circuit skipped. Any valid API key drew 200 for a
denied principal kind, a nonexistent id, another tenant's workspace, and
a request missing a required param, while the GET beside it answered 403
or 404. That is an existence oracle over MCP server ids, file ids, and
workflow ids.
`OperationUseCase` gains an optional `authorize()` that runs the phase
before the business transaction — allowed-principal check, canonical
load, asserted-scope comparison, current access check — and stops.
`defineAuthorizedWorkspaceUseCase` shares one implementation between it
and `execute`, so the two cannot answer differently. A HEAD on a
not-head-safe route is now admitted, parsed, and authorized like the GET,
rendering refusals through the route's own error policy, then answered
bodiless. The builders refuse at definition time to pair
`headSafe: false` with a use case that has no `authorize`, so the next
such route is a boot failure rather than a silent 200.
Separately, `parseRequest` validates the query slice only when the
contract declares one, so an omitted `query` means "never look at the
query string" rather than "takes no query params". 69 v2 contracts
omitted it and accepted anything: `?bogus=1` was a 200 on
`GET /workflows/{id}` and a 400 on every list. They now declare
`noInputSchema`, and 8 more contracts that declared a query without
`.strict()` are tightened. A sweep over the contracts tree is the
enforcement — a compile-time gate on `defineRouteContract` was tried and
reverted because the required intersection collapses inference of the
sibling generics.
Four route tests appended `?workspaceId=` to a PATCH/PUT that reads it
from the body; that copy was being silently dropped and is now a 400.
The generated specs are byte-identical: the OpenAPI generator learns that
a slice declaring no keys publishes no parameters.
* test(tables): pin the multiselect paste on the refusal, not the silent empty
cleanCellValue runs the same registry coercion the server does, so tightening
multiselect on the server changed this helper too. The case asserting an empty
array was pinning the silent-drop the tightening removed.
* docs(v2): make the API-key security description render as plain prose
The description was already published on every spec but did not appear in the
rendered Authorization block. It carried a raw > and backticks, which the
markdown pass in the docs renderer does not survive; the operation description
on the same page renders fine. Reworded to plain prose with the same substance.
* fix(v2): bind the query cursor to its filter on every shape
Two agents each fixed half of this: the shared list codecs gained filter
binding, and the table codec gained a fingerprint, but the pure-keyset shape
stamped it on neither encode nor decode. A keyset position is absolute in
(order_key, id), which is why it was left unbound — but absolute ordering is
not completeness. Replaying the cursor under a wider filter silently omits
every match sorting before it, so paging predicate A then B returned rows 7,9
where the full B sequence is 1,3,5,7,9.
Also answers a lost create race with the conflict it already documents, and
shortens three descriptions that dwarfed their siblings — the forbidden-code
catalogue now lives on the error envelope's details field, published once per
document instead of on all 135 operations.
* fix(tables): make a saved view's column references survive the write
A view config stores every column reference as a stable column id, but two
things wrote it in different vocabularies and nothing translated between them.
`config.sort` was pruned on read against the live column ID set while the
contract defines `sort[].field` as a column NAME, so every name-keyed sort —
the only kind the v2 surface can express — pruned to nothing and the view came
back with `sort: null`, on both create and PATCH, with no warning. The same
prune dropped a sort on `createdAt`/`updatedAt`/`id`, which are sortable row
columns that simply are not in `schema.columns`. `config.filter` had the
opposite failure: it was stored verbatim, so a predicate naming a column that
does not exist saved happily and then 400'd on every `/query`, `/query/count`,
and `/rows/find` that tried to use it.
The write path now canonicalizes a config before storing it: every column
reference (layout keys, `sort[].field`, each `filter` leaf `field`) is resolved
to the column's stable id, and `filter`/`sort` are validated against the live
schema so a reference that can never resolve is refused instead of saved. The
v2 read presents the config back keyed by column name, matching
`presentV2WorkflowGroup` and every other v2 row/data surface — a caller never
sees a `col_…` id, and what it wrote is what it reads. Resolution is a lookup
with pass-through, so the id-keyed first-party UI is unaffected.
Column LAYOUT stays unvalidated on write and pruned on read: it auto-saves as
the user drags, so racing a column delete must self-heal, not fail the drag.
The read path still never prunes a predicate, for the reason already documented
there — a pruned condition silently widens the view's row set.
* fix(storage): validate at the decode and multipart boundaries, bound derived keys
Four caller-reachable 500s shared one shape: input passed boundary
validation, then failed in the storage/key layer. Each is fixed at the
boundary that owns the transformation, not at the call sites.
Percent-encoded NUL in a canonical folder path. `parseRequest`'s NUL scan
sees `%00` as three ordinary characters; the NUL only exists after
`parseFolderPath` decodes it. Reads survived as 404s, writers carried the
decoded name into an INSERT and the driver threw. The rejection now lives
in `encodeFolderPathSegment`, the single chokepoint both building and
parsing funnel through, so it covers every escape a caller can spell.
NUL in a multipart field. A multipart route declares no body contract, so
its fields never reach contract validation at all — the knowledge-document
key was sanitized while `original_name` was not, and the object landed in
storage before the insert threw. `readFormDataWithLimit` is the shared
multipart reader every such route already funnels through, so the scan
goes there and runs before a caller holds a File to upload, which removes
the orphan rather than cleaning it up.
Storage-key overflow at 225 characters. Every generator embedded the file
name in a path component it also prefixed with a timestamp and a
uniquifier, so the effective limit was 255 minus that prefix while the
contract advertised 255 — a 225-character name produced a 256-byte
component and ENAMETOOLONG from local storage, and the upload session
handed out a transfer URL that could never succeed.
`buildStorageKeySegment` reserves the prefix out of the component's budget,
making the key independent of name length and the declared limit honest.
The NUL predicate is now shared from `@sim/utils/string` by all three
boundaries instead of being restated at each.
* docs(v2): make the published spec describe the API it has
Three descriptions asserted behavior the code no longer has, and three rules
the code enforces were published as unconstrained strings.
`downloadFile` and `listMcpServerTools` still told callers a `HEAD` on a
not-head-safe route "is answered with an empty 200 ... reports only that the
endpoint exists and the caller is authorized". That was true of the old
short-circuit, which sat between admission and parsing and therefore returned
200 for an id the same caller's `GET` refused. The builders now authorize a
HEAD exactly as the GET, so the spec said the opposite of a security fix. One
`HEAD_MIRRORS_GET` constant replaces both sentences and is added to
`exportWorkflow`, whose `headSafe: false` was never documented at all. A test
walks the `app/api/v2` tree for the declaration and fails on any operation that
carries it without the sentence, or that resurrects the old claim.
`createMcpServer` promised that re-registering an existing URL "rewrites the
configuration and returns the server to the same unverified state"; it is a
409 pointing at PATCH. `authType` claimed Sim "detects it from the server when
omitted" — registration deliberately never contacts the server, and the column
defaults to `headers`. The default stays: `headers` and `none` are
behaviourally identical (only `oauth` branches), so changing it is a migration
with no caller-visible payoff, while the sentence was simply false.
`predicate` was the API's most consequential gap: a `pipe` over `z.unknown()`
documents from its input, so the leaf keys `field`/`op`/`value` appeared
nowhere in the contract and `{column, operator, value}` was a 400 a caller
could not correct against. Both predicate schemas now publish a real recursive
JSON Schema through `.meta()`, self-referencing so the recursion resolves from
one `$defs` entry, with every bound read from the constant that enforces it.
Also published: the canonical folder-path rule and its 4096-byte cap on the
four path components (the `superRefine` contributed nothing to JSON Schema);
the closed 12-value `recursive` vocabulary on a destructive delete; and the
null-matching behaviour of the negating operators. The clamping `limit` branch
drops `minimum`/`maximum`, which in JSON Schema mean "rejected outside" and
made SDKs refuse locally what the server clamps.
`deleteFile` stops publishing a 409 nothing in its path can raise. `restoreFile`
and `abortFileUpload` keep theirs — the report called them unemittable, but
restore raises `FileConflictError` after exhausting its rename retries and
abort refuses a completed session.
Description tail, across the seven specs: p99 733 to 465, max operation 1643 to
1114, over 700 chars 31 to 13, over 400 70 to 61. Constraints moved from
operation prose onto the fields they constrain rather than being deleted.
* fix(v2): make upload completion, blank query values, search, and folder filters answer correctly
Four defects on the v2 surface, each reproduced before it was fixed.
Upload completion dispatched document indexing from inside the completion
transaction, so a queue or processing failure returned 500 after the object was
stored, the document row was created, and the session was marked completed —
and the only recovery, replaying the request, answered 200. The dispatch is now
a follow-on step that runs after the session is durably completed and is logged
rather than raised. Its outcome stays visible on the document itself (`failed`
with an error, or `pending` when it was never picked up), and the recovery path
re-queues a `pending` registration instead of keying off a message left on the
session.
A query parameter sent with no value was read as `0`, `false`, or the parameter
default: `?limit=` became `LIMIT 1` on the three lists that clamp, and
`?minCost=` on `/logs` became a live `cost >= 0` filter. `search` and `cursor`
already rejected a blank and documented "omit the parameter instead"; that rule
now applies to every v2 parameter, enforced on the raw query before coercion so
a parameter added later inherits it.
The document list matched `_` and `%` in `search` as live LIKE wildcards while
every sibling list escaped them through `searchFilter`, so the documented
substring match returned everything for `a_itest`. It now uses the same helper.
A `folderPath`/`folderPaths` naming no folder answered 404 on `/logs`, `/files`,
`/workflows`, `/tables`, and `/knowledge`, while every other filter answers an
empty page and the sibling folder lists already do. All five now return an empty
page. Mutations keep their 404.
* chore(v2): regenerate the specs from the merged sources
The four spec conflicts in the wave-3 merge were resolved by taking one side,
which left them describing neither branch. Regenerated so the published
documents match the contracts they are built from.
* docs(v2): give a built-in skill's id its real form
The contract said a built-in skill uses its name as the id. The ids are
`builtin-` plus the name, so a client following the description asks for
/skills/research and gets a 404 where the spec promises the skill.
* fix(uploads): keep local upload artifacts inside NAME_MAX
`POST /api/v2/files/uploads` accepted a name of up to 255 characters,
returned 201, and handed back a transfer URL that could never succeed:
the PUT against it 500'd and `complete` then reported the object missing.
The local provider named its staged object after the destination —
`{key}.{uploadId}-{uuid}.tmp` plus a `.upload-metadata.json` sidecar — so
the staged component was the key's length plus ~99 bytes of fixed
overhead. Past roughly 125 characters of name that crossed POSIX
`NAME_MAX`, and `ENAMETOOLONG` is not a `LocalUploadBodyError`, so it
escaped as a 500. Multipart `complete` built the same name and failed the
same way. Only local storage is affected; S3, Azure, and GCS have no
per-component limit.
`buildStorageKeySegment` already budgeted the key to 255, one layer above
where the overflow happened. Two changes close it at the layers that own
each suffix:
- Staged artifacts move to a `.staging` root and are named from the
upload id alone. A name derived from the destination inherits its
length and then adds to it; a fixed-width one removes the arithmetic
instead of re-budgeting it, so no suffix added here later can depend on
the caller's file name. The staging root is a cleanup sweep root, which
also reclaims artifacts that used to be orphaned beside the
destination.
- The durable sidecar is reserved out of the key budget centrally.
`LOCAL_UPLOAD_METADATA_SUFFIX` moves next to the budget that must
account for it, and the budget is derived from a list of sidecar
suffixes, so adding one shrinks every key builder at once.
The declared `maxLength: 255` stays honest: a 255-character name now
completes PUT and `complete` end to end.
* fix(uploads): budget every key built from a caller-supplied name
Auditing the rest of the codebase for the shape that broke the
upload-session PUT found five more key builders that put an unbounded
name into a path component local storage writes directly.
Three are on the same route as the original bug: `table_import`,
`profile_picture`, and `workspace_logo` built their key inline with
`sanitizeFileName`, which maps characters and never truncates, while
their sibling purposes went through `buildStorageKeySegment`. A
255-character name broke `table_import` at the metadata sidecar and the
other two at the object write itself.
The other two are local-storage writers reached from elsewhere:
knowledge-base connector sync capped the document title at 200 and then
appended a timestamp, a uuid and `.txt` on top of the cap, landing at
exactly 255 with no room for the sidecar; the Mistral-OCR staging and
chunk keys inlined the sanitizer with no bound at all; and inbound email
attachments went into a key with neither sanitizer nor bound, on a file
name an outside sender chooses.
All now derive their component through `buildStorageKeySegment`, so the
reservation is stated once. The upload-session test asserts it for every
purpose the contract admits, which is what keeps a newly added purpose
from reintroducing the hand-built form.
* fix(v2): stop the logs and billing reads answering 500 or a silent restart
Four caller-reachable failures on `GET /logs`, `GET /logs/{runId}`, and
`GET /billing/logs`, each fixed at the layer that owns the guarantee.
`minDurationMs`/`maxDurationMs` were published as `number` against an
`integer` column, so `1.5`, `-0.5`, `2147483648`, and `1e30` all reached
Postgres as bind parameters it refuses to parse. They are now whole
milliseconds bounded to int4, and the generated spec says so.
`0000-01-01T00:00:00Z` satisfies the published `date-time` pattern but
names no instant Postgres can store, since the proleptic Gregorian
calendar has no year zero. `v2RunWindowBoundSchema` now rejects it, which
covers both log families and the files-audit read that share the schema.
A scoped cursor whose inner token was the empty string passed the
`typeof === 'string'` envelope check and then read as falsy in every
domain reader, so both lists silently served page one again with a
`nextCursor` inviting another lap — the exact failure
`UNKNOWN_CURSOR_MESSAGE` exists to make visible. An empty inner is now
unreadable, and the sibling `decodePublicLogCursor` gets the same
treatment for its `id` half. The rejection message no longer names
`sortBy`/`sortOrder`, which neither operation accepts.
`GET /logs/{runId}` reported `folderPath: null` for both a workflow at
the workspace root and a folder it could not resolve, so a caller could
distinguish neither, and `null` is not a value `folderPaths` takes back
as a filter. The root is now `/`, matching the workflow resources.
Also, from the same audit: comma lists reject an empty entry the way
`folderPaths` already did instead of dropping it; a query param sent
twice is named as duplicated rather than reported absent; and the
`triggers=all` sentinel, the detail-level promotion by
`includeTraceSpans`/`includeFinalOutput`, and the 403/404 split against
the billing family are documented where each is decided.
* fix(v2): pin naive timestamps to UTC and close six contract divergences
Application-written timestamps reached the wire as a local wall clock
labelled `Z`. Every column in `schema.ts` is `timestamp without time
zone`, so the instant a value denotes was decided by whoever wrote it and
whoever read it, and the writers disagreed: `now()` renders in the
session's TimeZone, drizzle's `mapToDriverValue` is `toISOString()`, and
a raw `Date` bound through postgres.js is cast down in the session's
TimeZone. The read side disagreed the same way — postgres.js parses oid
1114 with `new Date(x)`, which is the process's local zone, while a value
it hands back as a string is read as UTC by drizzle. The result passes
every `date-time` check, so it silently corrupts sorts and range
predicates and can place `updatedAt` before its own `createdAt`.
`packages/db/timestamps.ts` removes the ambiguity at the driver boundary
rather than at the call sites: the session TimeZone is pinned to UTC so
all three write paths store the same wall clock, and oid 1114 is parsed
as UTC so every read path recovers that instant. `withUtcTimestamps`
merges both into a client's options, because `connection` is nested and a
pool setting its own `application_name` would otherwise drop the
TimeZone. Production already runs both in UTC, so nothing changes there;
every other environment now behaves the way production does.
Alongside it, six places where the published contract and the code
disagreed:
- Multi-select `ncontains` was documented as "the exception" that
excludes nulls. It never did, and no test claimed it did — `data` is
never NULL, so containment is false for an absent key and the negation
is true, exactly like every other negation. The sentence was wrong.
- `recursive` published twelve lowercase spellings while `z.stringbool()`
folded case, so the server honoured `recursive=True` as a destructive
recursive delete that a generated client would have refused to send.
Narrowed to case-sensitive: accept exactly what is published.
- The upload data plane answered with a bare `{ error: string }`. Being
absent from the OpenAPI documents is a statement about addressability,
not about behaviour; both PUTs now use the canonical envelope, and what
the transfer step promises is published on `transfer.url`.
- Full-set lists told callers to "send it back as `cursor`" on a
`.strict()` query that rejects `cursor`. `v2CursorListResponse` now
takes `paged`.
- A `HEAD` on a download skips the read that produces `Content-Length`,
so it cannot size a download; the description says so.
- The upsert conflict-target rejection echoed the storage id a name-keyed
surface had already translated to, and the scoped-cursor 400 named
`sortBy`/`sortOrder` params `/audit-logs` does not accept.
* improvement(v2): cut the extraneous half out of the published descriptions
The v2 spec's description median was already healthy at 42 characters; the
tail was not. 174 descriptions ran past 200 characters and 13 past 700,
almost all of it rationale, cross-references, and constraints restated on
the wrong object.
Trim the shared error, folder-path, retention, pagination, and workspace-key
constants first, since each is published on between two and twenty-seven
operations. `FOLDER_TREE_TOO_LARGE` dropped the clause explaining why the
tree has to load, `FULL_SET_LIST` dropped a second sentence restating its
first, `RUN_RETENTION` dropped the `runCount` caveat that already lives on
`runCount`, and the 503 and 499 descriptions dropped the paragraphs
narrating why they are documented at all. That reasoning belongs in the
TSDoc beside each constant, which is where it now is.
Then the operations. Execute Workflow and List Runs each restated a rule
their own parameters already carry — the `X-Run-Id` uniqueness claim and the
`order` sort deviation — so both moved to the parameter that owns them. The
run-status enum sent a caller to `paused.automaticResumeWaitingReason` and
then explained that field in place of describing it; the explanation moved
onto the field, which previously said only that it was "the reason automatic
resume is waiting".
Align the parameter vocabulary a caller meets in every family. One `cursor`
description had forked on the table row query, one `sortBy` on knowledge
documents, and the table row `limit` published neither its bounds nor its
default. `nameSortCollation` is now a function of the column it names, so
the knowledge document list can state the caveat about `filename` without
claiming a `name` field it does not have. `scripts/openapi/documents.test.ts`
pins `cursor` and `sortOrder` to one string each, and the retention window to
both reads that publish it.
Distribution over the seven documents: mean 71 to 67, p95 223 to 199, p99 453
to 370. Over 200 characters 174 to 147, over 300 94 to 59, over 400 54 to 20,
over 700 13 to 9. The median is unchanged at 42.
* fix(v2): keep one unreadable-cursor message
Two branches each added the constant, in cursor-binding and list-query. It
belongs beside its sibling REFILTERED_CURSOR_MESSAGE, so the list-query copy
and its importers move there.
* fix(v2): bind a cursor to what a set filter means, not how it was spelled
workflowIds, triggers and folderPaths are comma lists the query treats as
unordered sets, and tagFilters is an object whose key order carries no meaning.
Fingerprinting the raw spelling bound the cursor to the spelling, so a caller
who reordered an equivalent filter mid-walk got a 400 for a page that was
genuinely the next one.
* fix(v2, db): make two unfalsifiable tests observable and document strict query
Three follow-ups on the w5 policy work: one decision recorded, two tests that
could not fail.
The `query: noInputSchema` sweep is kept. It is a real tightening — 69 v2
operations that ignored an unknown query param now answer 400 — so it was
weighed rather than assumed. The v2 body slice on those same endpoints was
already `.strict()`, and every v2 list already rejected `?bogus=1`, so the
split was arbitrary rather than a promise: the same typo was a 400 on
`GET /workflows` and a silent 200 on `GET /workflows/{id}`. A parameter the
server drops without saying so is the bug class the lists' rule already exists
to prevent. No first-party caller is affected — the two SDKs send only
`includeOutput`/`selectedOutputs`, both declared; the UI and the desktop app
make no v2 calls at all; `requestJson` appends nothing implicitly and no v2
cache buster exists; every docs example uses a declared param. A third-party
caller appending a tracking tag does break, which is why the behavior is now
documented in the API reference with the exact 400 body rather than left to be
discovered, and why the reasoning sits in the v2 conventions skill next to the
rule instead of only in a commit message.
`packages/db/timestamps.test.ts` asserted that `withUtcTimestamps` registers a
UTC parser on oid 1114 by reading it off a bare postgres.js client. Every real
client is then handed to `drizzle()`, which overwrites that entry with a
transparent parser, so the assertion held whether or not the parser had any
effect. The mechanism is fine and stays: drizzle's own `PgTimestamp` mapper
appends `+0000`, so the read is UTC-correct either way and the session
`TimeZone` pin — the write-side fix — is untouched by `drizzle()`. The test now
resolves the parser both before and after `drizzle()`, pins the clobbering it
depends on, and asserts the instant recovered through the full composition, so
a regression in either layer is red. `timestamps.ts` records why the inert
entry is kept.
`nul-byte-boundary.test.ts` embedded a raw U+0000, so git classified it binary
and rendered it as `Bin 0 -> 4102 bytes` — the test proving the NUL hardening
works was the one file a reviewer could not read. The escape is byte-for-byte
equivalent at runtime. Two older files had the same defect and are fixed the
same way. `check:source-text` now fails the build on a raw NUL in any tracked
source file, and `.gitattributes` forces source files to diff as text so the
next one is visible in review rather than hidden by it.
* fix(w5): narrow three fixes that reached past the harm they were fixing
The workflow-create `23505` handler answered for the whole transaction, which
also runs `saveWorkflowToNormalizedTables`. `workflow_blocks.id` is a global
primary key, so a block-id collision — an integrity fault already seen in
production — surfaced as `A workflow named "X" already exists in this folder`.
Match on the constraint name; any other unique violation propagates unchanged.
Moving the knowledge dispatch out of the completion transaction was right, but a
dispatch failure then committed the session as `completed` and left the document
at `pending`, which nothing sweeps and `retryProcessing` refuses. Record the
failure on the document instead, so it lands on the existing failed-document
path, and describe what the code does rather than a recovery branch that cannot
fire for this state.
The MCP re-registration reset stopped a registration claiming a connection it
never made, but reset for any re-registration. `isServerEligibleForDiscovery`
skips an OAuth row that is not `connected`, so a rename removed every tool the
server published with no path back. Scope the reset to url, transport, headers,
auth type, OAuth credentials, and revival.
* fix(tables): confine the write-policy tightening to what the caller sent
The null-policy work made `reject` the default for caller-supplied writes,
which is right, but it landed on the wrong values.
- A partial update coerces the MERGED row, so an untouched legacy cell failed
an unrelated column's update — and failed a paged bulk job after its earlier
pages had committed. The merged-row callers now name the patch's keys; every
other key follows the `null` policy, in the in-memory copy only (the write
sends the patched keys alone).
- A multiselect whose members do not all resolve returned `{ok:false}`, which
on the machine paths that pass `'null'` — CSV import, computed writes, the
cell-write snapshot — erased the whole cell. Those paths now consult a new
`salvage` hook and keep the members that do resolve; a caller-supplied write
still 400s on an unknown option.
- Refusing a bare number in `date.coerce` reached the executor, v1, copilot and
the grid. The refusal stays where there is a caller to tell, and `salvage`
restores the milliseconds reading where the only other answer is a blank cell.
Also: the cursor docblocks claimed pure-keyset cursors were left unbound while
the code and its tests bind them; a saved-view create took the table's SCHEMA
advisory lock, so it queued behind column rewrites whose statement timeouts run
past its 3s lock_timeout, and now takes a views-scoped lock instead; and a view
whose column was deleted could not be saved at all, because the Save chip always
resends the filter — references the stored config already carries are now exempt
while a newly introduced one is still refused.
The cursor version is deliberately not bumped: the stamp is additive, unfiltered
in-flight tokens keep working, and a filtered one fails with the accurate
"restart paging without the cursor" rather than a generic unreadable-cursor 400.
* test(db): narrow the mapped timestamp to Date
mapFromDriverValue is typed unknown, so the composition assertions did not
type-check outside the test's own runner.
* fix(v2): correct four stale contracts and clear the merge debris behind them
Five of the reported defects were real and four of them were documentation
that had stopped describing its own code.
`cleanCellValue` said only "coerce a raw input value"; it also answers `null`
for anything the column type refuses, and since the multiselect write path
started refusing partial matches that is the difference between a paste
storing one option and blanking the cell. It deliberately does not consult
`salvage`, which would read the same paste as the option that did resolve —
that reading is for writes with no caller to answer, and a typed cell has one.
The pairing is now asserted, so a future helper that "improves" the paste by
salvaging it fails.
`EXECUTE_OPTION_CONSTRAINTS` carried two stacked TSDoc blocks, the second
explaining that the enumeration had moved onto the fields; the body schema
still told a reader the six combinations were enumerated in the constant. The
deployment route's second block orphaned the endpoint documentation above it,
and `list-query.ts` kept the TSDoc for a cursor message that now lives, with
its own rewritten doc, in `cursor-binding.ts`. Two agents left near-identical
essays arguing the same 400-vs-403-vs-409 question about the table ceilings
and concluding that neither status changes; the decision is recorded once, in
`billing.ts`, and `service.ts` points at it.
The credentials use case echoed `sortBy`/`sortOrder` back with a TSDoc
explaining that the presenter needs them, which it no longer does — it reads
`query.*`. The local upload roots move from the data-plane provider to
`core/storage-key.ts`, beside the sidecar suffix, so the cleanup sweep can name
what it reclaims without importing the transport that writes it.
`documents.test.ts` justified sweeping only knowledge and files for the 413 by
saying the same sweep over the other five documents still reported gaps. It
does not: widened to all seven, every body-carrying operation publishes it.
Three reports did not survive checking, and the evidence is recorded where the
next reader will look. An empty rerank result is not the reranker matching
nothing — `rerank` asks for `top_n` over a non-empty document list, so an empty
array means the response carried nothing usable, which is what `unavailable`
already promises. The zero-byte knowledge document is refused on the
upload-session path too, by `validateFile`, under both boundary contracts;
that parity is now pinned, and it fails if the guard is removed. The MCP
re-registration reports exactly the connection fields its SET clause writes,
and the create mutation already drops both caches — what lags is the status
badge, not the tools, because discovery is gated on `connected` for OAuth rows
only.
* fix(v2): de-duplicate a set filter before fingerprinting it
The filters compile to inArray, which is set membership, so workflowIds=A,A,B
selects exactly what A,B does. Sorting alone still bound them to different
pages, so an equivalent filter with a repeated member 400d mid-walk.
* fix(w6): close a head-authorization hole, a TZ leak, and five tests that could not fail
Six risks an adversarial read of this week's diff raised, verified one at a
time. Two of the six were already correct and are reported as such rather than
changed.
`v2HeadAuthorizationResponse` optional-called the use case's authorization
phase, so a use case without one would have answered the bodiless 200 that
`headSafe: false` exists to prevent. The definition-time guard does cover both
builders that reach it — they are its only callers — but an optional call turns
a missing phase into that leak silently, so the responder now refuses instead
of skipping.
`packages/db/timestamps.test.ts` assigned `process.env.TZ` at module scope and
never restored it. `TZ` is process state: a worker running files back to back
carried Asia/Tokyo into every file that followed, and only when the ordering put
it after this one. The zone is now set and restored around the file, with both
properties the suite depends on intact.
Upload publication moved its staging area out of the destination's own
directory into a shared `.staging` root, which makes the publishing `link` a
cross-subtree one. A volume mounted under part of the uploads tree puts the two
on different devices and `link` answers `EXDEV`, which the same-directory link
could not. Publication now copies onto the destination's device and links from
there, keeping the create-or-fail step that stops a replay from overwriting a
stored object.
Five tests that passed regardless of the code:
- `resolveFolderPathFilter` was only ever exercised through hand-written
reimplementations in the suites that mock it out, so widening a miss to
unfiltered — every filtered list answering with the whole workspace — left
them all green. The real helper is now tested where it lives.
- The only measurement of `generateWorkspaceFileKey` asserted the key's last
component against `NAME_MAX` rather than the component plus the sidecar
written beside it, so it passed with the sidecar reservation removed.
- `GET /logs` asserted only that a rejected cursor does NOT name `sortBy`,
which almost any wording satisfies, including one saying nothing at all.
- The skills lifecycle test asserted that the four writes agree on a
workspace-key policy, which a lifecycle uniformly allowing one also
satisfies; it now pins the policy they agree on and the kinds they admit.
- The v2 skills create test lost `expect(capture).not.toHaveBeenCalled()` when
the create path moved to a personal key. The behaviour it pinned is gone —
the workspace-key create is refused now — so it is re-homed as the refusal
reaching the caller as a 403 with no analytics behind it.
Two claims did not hold. `CURSOR_VERSION` is correctly left at 1: the filter
stamp is additive, a pre-stamp token still decodes, an unfiltered read still
resumes, and only a filtered replay fails — with a conflict that names the
filter, where a version bump would answer a generic unreadable-cursor 400 to
every in-flight token. Tests pin all three, plus the minted version itself. And
the upload-session key-budget cases do exercise the real shared budget through
the real segment builder; only the workspace-key prefix is the stub's, which is
now stated where the stub is declared.
* refactor(v2): collapse two names for the cursor scope key onto one helper
`cursorFilterScope` in the v2 response module was a one-line pass-through to
`cursorScopeKey` in `lib/api/cursor-binding`, so the same function was reachable
under two names from two modules. Routes now call `cursorScopeKey` directly, the
way they already import `unorderedScopePart` and the cursor messages from that
module, and the wrapper plus its duplicated doc comment are gone.
Also folds the `id -> name` column map in the v2 tables presenter onto
`buildColumnNameById`, which the same file already imports and calls thirteen
lines above; restores two doc comments that had drifted onto the wrong
declaration; and replaces three `as Date` casts in the timestamp test with
`toEqual(new Date(...))`, which needs no cast and additionally fails when the
mapped value is not a Date at all.
* refactor: delete three pieces of surface this branch added with no consumer
`v2CursorSchema` had one caller, `v2PaginationFields`, in the same file, and its
only parameter was a default nobody overrode — so the export and the parameter
were both unreachable. Inlined into the pair it belongs to; the emitted schema
and its description are byte-identical, so the generated OpenAPI does not move.
`PatchedKeys` was declared `ReadonlySet<string> | readonly string[]`, but all
four callers pass `Object.keys(...)` and no test passes a set, which left the
`instanceof Set` arm of `policyResolver` unreachable. Narrowed to the array form
the callers actually use.
`NUL_CHARACTER` was exported from `@sim/utils/string` and imported by nobody —
every boundary imports `containsNulCharacter` instead. Kept as the module-local
constant the predicate reads, dropped from the package surface.
* docs(v2): state why the local upload data-plane routes bypass the builders
Both local-storage PUT routes use raw `withRouteHandler`. The global rule
allows that only for documented protocol or lifecycle exceptions, and their
TSDoc explained the OpenAPI exemption and the error envelope but never the
builder bypass itself. Record the actual reason: a signed `upload-token` is
the credential, so there is no API key, `Principal`, or semantic operation
for a builder to authenticate and authorize against, and the body streams
straight to storage rather than being parsed.
* test(v2): pin cursor-to-filter binding on the tables and runs lists
The branch binds every paged cursor to the filters it was minted under, but
the binding was enforced end-to-end on only 4 of 16 paged lists. The
contract-level CURSOR_BINDINGS sweep looks like the safety net and is not:
it checks each contract against a hand-maintained map of param names, never
against what a route actually stamps into cursorScopeKey, so it stays green
for a route that dropped the stamp entirely.
Confirmed by deletion. Removing tableCursorFilters from both call sites on
GET /v2/tables left all 8 tests passing, and the runs route was worse — its
one relevant assertion was weakened from toEqual to toMatchObject in this
same branch, leaving the new filter field unpinned.
Adds a mint-then-replay test to each: a cursor minted under one filter set
and replayed under another is a 400 that never reaches the use case, with a
same-filter resume case as the control so the 400 cannot be satisfied by
blanket rejection. Restores toEqual on the runs cursor payload, pinning that
a filter is stamped without hardcoding the fingerprint.
Both new guards were verified to fail: removing the binding reddens the
refiltered test on tables, and both the refiltered and the re-armed toEqual
test on runs.
* fix(tables): keep the v2 write strictness inside v2
The write-path tightening on this branch changed shared code that every
first-party surface reaches, so the workspace grid, the internal
`/api/table` routes, `/api/v1`, the Copilot table tools, and the executor's
Table block all inherited a contract only `/api/v2` publishes. Each of them
now behaves exactly as it does on staging again, and v2 keeps the strictness
by opting into it.
- `coerceRowValues`/`coerceRowToSchema` default to the `null` policy again —
an uncoercible optional cell is blanked and the row is written. `reject` is
reached through `RowWriteOptions.uncoercibleValues`, which the v2 row
routes set via `strictWrite` on the application input.
- The same `strictWrite` scopes the unknown-column refusal to v2. Copilot
feeds the model's raw arguments in unfiltered, so a hallucinated key, an
echoed `id`, or a name left over from a rename had begun refusing the whole
write.
- Multiselect and bare-epoch values land again for first-party callers
through the registry's existing `salvage` hook, which the `null` policy
already consults; the grid's `cleanCellValue` consults it too, so a paste
naming one live option and one deleted one keeps the live one instead of
erasing the cell.
- The saved-view name→id remap no longer rewrites a ref that already means
something else, so a user column named `id`/`createdAt`/`updatedAt` cannot
hijack a view's system-column sort or filter.
- `createTableView` tolerates the refs its own config carries unless the
caller is strict, so "Save as view" stops 400ing on a dangling filter the
Save chip accepts.
- The bulk update runner is byte-identical to staging again.
The 100-view cap stays: the list read is unpaginated, so the promise it makes
only holds if the write side enforces it, and it refuses a new view rather
than an existing config.
* test(v2): pin cursor-to-filter binding on seven more paged lists
Extends the mint-then-replay guard from tables and workflow runs to the
remaining paged v2 lists the audit found with no route-level coverage:
credentials, audit-logs, custom-tools, mcp-servers, secrets, knowledge
bases, and knowledge documents.
Each gets a cursor minted by driving GET under one filter and replayed
under another, asserting a 400 carrying REFILTERED_CURSOR_MESSAGE that
never reaches the use case, plus a same-filter resume control so the 400
cannot be satisfied by blanket rejection. The three cursor schemes are all
covered: keyset (readSortedCursor), the scoped wrapper audit-logs uses for
its domain token, and the offset cursor on knowledge documents.
The documents suite had no GET coverage at all, so its list use case gains
a real mock and the route's GET export a describe block.
All fourteen were verified to fail: dropping the cursor-filter argument
from both call sites on each route reddens exactly that route's refiltered
test and leaves every other assertion in the file green, which is the
failure mode the contract-level CURSOR_BINDINGS sweep cannot see.
* test: cover four untested behaviors and drop five tests that cannot fail
Adds coverage that goes red when the behavior is reverted:
- `rejectDuplicateQueryValues` through `parseRequest`, not just the pure
helper — the existing blank-query tests stay green even when parseRequest
ignores the flag entirely.
- `failUndispatchedDocumentProcessing`'s pending + not-deleted WHERE guard,
asserted on the condition tree so removing it fails.
- The widened `present(result, request)` signature, so dropping the second
argument stops being a silent no-op.
- The NUL scan on `readFormDataWithLimit`'s content-length branch — the
branch every ordinary browser and curl upload takes, and the one the
existing multipart tests never reached.
Removes tests verified incapable of failing: the credentials projection row
(the outbound `.parse()` strips unknown keys either way), the per-document
413 sweep (vacuous on two of three documents, subsumed by the sweep in
scripts/openapi/documents.test.ts), the two upload-session rows that assert
their own `generateWorkspaceFileKey` stub, the storage-key row whose 20-byte
name never reaches the budget, and the views-lock assertion against a
function `views/service.ts` does not import.
* fix(v2): parse a bound list filter once, so the scope matches the query
The logs list fingerprinted `workflowIds`, `triggers`, and `folderPaths`
through unorderedScopePart, which trims each member, then split the same raw
values itself with `.split(',').filter(Boolean)`, which does not. So
`?workflowIds=A,B` and `?workflowIds=A, B` produced one fingerprint and two
different result sets: the second selects on a member with a leading space
that matches no row. A cursor minted under one was accepted under the other,
which is the exact failure the filter binding exists to refuse.
Extracts parseUnorderedList as the single parse. unorderedScopePart now
derives from it, and the route passes the array to the query and the joined
form to the scope, so the members fingerprinted are by construction the
members filtered on. Also drops three inline splits.
Reported by Greptile.
* fix(v2): bind an AND-conjoined filter array as a set, not a sequence
The knowledge documents list fingerprinted tagFilters through canonicalJson,
which sorts object keys but preserves array order. Each filter compiles to a
condition in and(...whereConditions), and AND is commutative, so the same
clauses written in a different order select the same documents — and got a
different fingerprint, refusing a cursor for a page that was genuinely the
next one.
Adds unorderedJsonScopePart beside parseUnorderedList: members are
canonicalized, de-duplicated, and sorted, so `A AND A` binds like `A` and
clause order stops mattering. A non-array or unparseable value still binds
by its raw spelling, since that request fails validation anyway.
Replaces the route-local canonicalTagFilters, and corrects the claim on
canonicalJson that array order only ever costs a restart — for a set-valued
filter it costs a spurious 400.
Reported by Greptile.
* fix(v2): bind list filters by the value the query acts on, not its spelling
Third report of one root cause, so this fixes the cause rather than the case.
A cursor scope must fingerprint what the query filters on; every place it
fingerprinted the caller's raw text instead, two spellings of one filter got
two scopes and a valid next page got a 400.
Knowledge documents: tagFilters bound the raw query text while the route
already parsed it two lines below for the use case. The schema defaults
operator to 'eq', so {tagName,value} and {tagName,value,operator:'eq'} are
one filter to the query and were two scopes to the cursor. The scope now
binds the parser's output, which also subsumes the clause-order fix — both
route tests go red against the raw-text form.
Logs and workflow runs: startDate/endDate bound the raw text, but
z.string().datetime() admits every sub-second spelling of one instant, so
`…00Z` and `…00.000Z` name one window and got two scopes. New
instantScopePart binds the parsed instant.
Replaces unorderedJsonScopePart, which took raw text and could not see a
schema default, with unorderedScopeOf over the parsed value.
Swept all fourteen routes that build a cursor scope for the same divergence;
these were the only ones where a scope part is derived differently from the
value reaching the use case.
Reported by Greptile.
* fix(v2): bind the audit and billing window bounds by instant
The previous sweep for this defect looked for a transform in mapInput, so it
missed the two routes that pass their raw bounds to a use case that parses
them deeper. Both fingerprinted startDate/endDate as text while their
predicates convert to a Date, so `…00Z` and `…00.000Z` name one window and
got two scopes, refusing the genuine next page.
Billing keeps stamping the raw params rather than resolveDateRange's output,
for the reason already recorded there: a relative `period` resolves against
the clock, so hashing the resolved window would reject every next page.
Normalizing the explicit bounds is compatible — instantScopePart is a pure
function of the caller's own text and resolves nothing.
Re-swept all fourteen cursor-scope routes by scope part rather than by
transform site. Every temporal and structured part now binds canonically;
the rest are enums and identifiers with one spelling per value.
Reported by Greptile.
* fix(v2): drop an inert field from the document tag-filter scope
resolveKnowledgeTagFilters builds every structured filter with the stored
definition's fieldType and never reads the caller's — not for resolution, not
for validation, not in its output. Fingerprinting it made a field the query
ignores decide whether a cursor resumes, so adding or removing a matching
fieldType refused a page that had not moved.
Swept the other twelve cursor-scope routes for the same shape. No scope part
is absent from its mapInput, this was the only scope carrying a structure
resolved against stored state, and knowledge/search has no cursor at all.
Reported by Greptile.
* refactor(v2): derive the body 413 from the contract in every document
Two mechanisms encoded one rule. `withRequestBodyErrors` derived the 413 from
`route.contract.body` for the tables document, while the resources document
hand-picked RESOURCE_BODY_ERRORS / RESOURCE_CONFLICT_BODY_ERRORS at nine
sites. The cross-document sweep caught drift, but only after the fact: a new
body operation that forgot the _BODY_ variant published a reachable 413
nowhere until a test failed.
Hoists the mapper to openapi/shared.ts and applies it in both documents, so
the rule is derived rather than remembered. The two hand-picked sets and
their shared TSDoc are gone.
Regenerating all seven specs produces zero drift, which is the proof the two
mechanisms were computing the same thing.
* refactor(v2): collapse duplicated cursor and validation mechanisms, drop dead exports
One rule, one implementation:
- `parseRequest` hand-inlined the "caller envelope or default" validation-error
projection four times. Extract `projectValidationError` and route all four
through it.
- Nine keyset lists hand-rolled the `present` half of the cursor pair that
`readSortedCursor` already owns the read half of. Add the symmetric
`writeSortedCursor` and use it everywhere.
- `GET /workflows/{id}/runs` re-derived `readSortedCursor`'s invalid/refiltered
ladder from `decodeSortedCursor`; it now calls the shared reader and keeps
only the key-arity check that is genuinely its own.
Files and exports that no longer earn their place:
- Inline `credentials/utils.ts` into its single consumer.
- Delete symbols with zero references repo-wide: `v2CustomToolWriteError`,
`secretCredentialTypes`, `v2CursorList`, `v2WorkspaceAccessError`,
`resolveFolderPathIdentity`, `folderPathForId`, `v2FolderPathMutationError`,
and seven of twelve `tables/utils.ts` exports.
- Drop `export` from symbols used only inside their own module.
No behavior change; every response body and error message is byte-identical.
* docs(v2): cut duplicated and non-load-bearing comment prose
Five rationales were written three to five times each by parallel agents
that could not see one another. Each now has one home and the rest point
at it:
- HEAD existence oracle -> the headSafe option on defineV2JsonRoute
- cursor query binding -> cursorScopeKey in lib/api/cursor-binding.ts
- storage-key prefix budget -> buildStorageKeySegment
- NUL / U+0000 -> the containsNulCharacter predicate
- blank and duplicate query values -> their own implementations
Also drops changelog-in-source (prose narrating what the code used to
do), anchorless module headers attached to no declaration, rejected-
alternative essays, and @param tags that only restate the signature.
Comments only: the diff contains no executable-code change.
* fix(v2): name the undecodable-cursor failure on the two sortless lists
GET /workflows/{id}/versions and GET /workspaces/{id}/members threw a bare
'Invalid cursor' literal where every other v2 list uses a shared constant.
The right one is UNREADABLE_CURSOR_MESSAGE, not INVALID_CURSOR_MESSAGE:
both lists take only limit and cursor, so naming sortBy/sortOrder would
answer one 400 with advice that earns a second.
Their missing filter scope is correct and stays. Neither contract accepts a
filter — v2PaginationFields is the whole query — so there is nothing to bind,
and limit is excluded from a scope by design.
Pins the message on the versions route, verified to fail against the literal.
* test(openapi): give the determinism check a chosen timeout
`serializes all documents deterministically` serializes all seven published
documents twice — roughly 2MB of JSON — under vitest's 5s default, which is
not a budget anyone picked for it. The published specs grew 3.3% on this
branch (961KB -> 993KB) from richer descriptions, which is far too small to
move a comfortable test and is enough to tip one already sitting just under
the cap. Measured at 5.1s in isolation with nothing else running.
Raises it to 30s for the openapi suite rather than trimming a real assertion.
* fix(v2): make the NUL path scan linear, and force a write surface to choose
Two findings from a simplify pass, both in code this branch added.
findNulBytePath copied `[...path, key]` per child, which is O(nodes x depth).
A caller controls that depth directly: v2 row cell values are `z.unknown()`,
so nesting passes Zod untouched and reaches the scan. Measured on Node 22 --
JSON.parse accepts a 200KB body nested 100k deep in 9.8ms, and the scan then
blocked the event loop for 27.7s. Frames now carry a parent link and the path
is materialized once, for the node actually reported: 27.7s -> 5ms, with
byte-identical paths across nested arrays, records, NUL keys and clean input.
The always-run first pass drops Object.entries for Object.keys, which halves
its cost on large bodies by not allocating a pair array per object.
`strictWrite` was optional with the lenient default, so a v2 write route added
tomorrow would silently inherit first-party behavior -- unknown column dropped
under a 201, uncoercible cell stored as null -- defended by nothing but five
copies of a literal. It is now required on the five write-shaped inputs, so
omission is a compile error. The type-checker named every caller: the five v2
routes already passed true, and the three Copilot sites now say false
explicitly, which is the behavior they already had.
* refactor(v2): apply the body-413 mapper to every OpenAPI document
The earlier unification wired withRequestBodyErrors into two of the five
content documents and left files-audit, knowledge and workflows hand-writing
the entry, so the helper's own claim that "a new body route cannot forget it"
held on 40% of the surface while reading as global.
Regenerating all seven specs produces zero drift, which is the useful proof:
the mapper agrees with every hand-written entry today, so the gap was never a
missing 413 — it was a missing guarantee for the next body route added to
those three documents.
The existing hand-written entries stay. The mapper is one-directional and
several bodyless folder reads publish 413 for the folder-tree ceiling, so
stripping them by hand would risk removing one the mapper cannot restore.
* refactor(v2): fold the v2 validation renderer into the shared parse defaults
V2_PARSE_DEFAULTS calls itself "the parse failures every v2 route renders the
same way", but the option deciding how a v2 validation failure renders sat
outside it and was re-stated at seven sites. A raw route that spread the
defaults and stopped emitted a non-v2 error envelope.
Removes the redundant line from the five sites that only restated it. The two
builders keep theirs: theirs sits after `...options.parseOptions`, so it is a
deliberate override that stops a caller swapping the v2 renderer, not a copy.
Also adopts the mandated `filterUndefined` in cursorScopeKey in place of the
Object.fromEntries/Object.entries form CLAUDE.md forbids, and collapses a
one-element `as const` array plus a Math.max over it to the single `.length`
they computed.
* test(persistence): keep the wire round trip without tripping the utils audit
check:utils forbids `JSON.parse(JSON.stringify(...))` and points at
structuredClone, which is right for a deep clone and wrong here: this test
exists to prove the schema accepts a `deployedAt` that arrived over HTTP as a
string as well as an in-process `Date`. structuredClone preserves the `Date`,
so adopting it would leave the test asserting nothing about the wire form.
Splits the serialize and the parse into two statements. The round trip stays
lossy — verified `JSON.parse(JSON.stringify(...))` yields a string where
structuredClone yields a Date — and the pattern the audit matches is gone.
Arrived from staging in #6660, so `check:audits` is red on origin/staging too,
not only here.
This commit is contained in:
@@ -171,6 +171,24 @@ The API uses standard HTTP status codes. v2 errors include a stable code and hum
|
||||
| `404` | Resource not found | Verify the ID exists and belongs to your workspace |
|
||||
| `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header |
|
||||
|
||||
### Unrecognized fields are rejected
|
||||
|
||||
Every v2 endpoint validates the request against its published schema — path parameters, query string, and body — and answers `400` for any field it does not declare. A misspelled parameter is an error rather than a silent no-op, so `?limt=20` fails instead of quietly returning an unbounded list.
|
||||
|
||||
This holds for endpoints that declare no query parameters at all. Do not append tracking tags, cache busters, or other extra parameters to a v2 URL; send only what the endpoint documents.
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "Invalid request",
|
||||
"details": [
|
||||
{ "code": "unrecognized_keys", "keys": ["limt"], "path": [], "message": "Unrecognized key: \"limt\"" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Use [Get Billing Status](/api-reference/billing/getBillingStatus) to inspect current credit and storage usage.
|
||||
</Callout>
|
||||
|
||||
@@ -171,6 +171,24 @@ The API uses standard HTTP status codes. v2 errors include a stable code and hum
|
||||
| `404` | Resource not found | Verify the ID exists and belongs to your workspace |
|
||||
| `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header |
|
||||
|
||||
### Unrecognized fields are rejected
|
||||
|
||||
Every v2 endpoint validates the request against its published schema — path parameters, query string, and body — and answers `400` for any field it does not declare. A misspelled parameter is an error rather than a silent no-op, so `?limt=20` fails instead of quietly returning an unbounded list.
|
||||
|
||||
This holds for endpoints that declare no query parameters at all. Do not append tracking tags, cache busters, or other extra parameters to a v2 URL; send only what the endpoint documents.
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "Invalid request",
|
||||
"details": [
|
||||
{ "code": "unrecognized_keys", "keys": ["limt"], "path": [], "message": "Unrecognized key: \"limt\"" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Use [Get Billing Status](/api-reference/billing/getBillingStatus) to inspect current credit and storage usage.
|
||||
</Callout>
|
||||
|
||||
@@ -171,6 +171,24 @@ The API uses standard HTTP status codes. v2 errors include a stable code and hum
|
||||
| `404` | Resource not found | Verify the ID exists and belongs to your workspace |
|
||||
| `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header |
|
||||
|
||||
### Unrecognized fields are rejected
|
||||
|
||||
Every v2 endpoint validates the request against its published schema — path parameters, query string, and body — and answers `400` for any field it does not declare. A misspelled parameter is an error rather than a silent no-op, so `?limt=20` fails instead of quietly returning an unbounded list.
|
||||
|
||||
This holds for endpoints that declare no query parameters at all. Do not append tracking tags, cache busters, or other extra parameters to a v2 URL; send only what the endpoint documents.
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "Invalid request",
|
||||
"details": [
|
||||
{ "code": "unrecognized_keys", "keys": ["limt"], "path": [], "message": "Unrecognized key: \"limt\"" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Use [Get Billing Status](/api-reference/billing/getBillingStatus) to inspect current credit and storage usage.
|
||||
</Callout>
|
||||
|
||||
@@ -171,6 +171,24 @@ The API uses standard HTTP status codes. v2 errors include a stable code and hum
|
||||
| `404` | Resource not found | Verify the ID exists and belongs to your workspace |
|
||||
| `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header |
|
||||
|
||||
### Unrecognized fields are rejected
|
||||
|
||||
Every v2 endpoint validates the request against its published schema — path parameters, query string, and body — and answers `400` for any field it does not declare. A misspelled parameter is an error rather than a silent no-op, so `?limt=20` fails instead of quietly returning an unbounded list.
|
||||
|
||||
This holds for endpoints that declare no query parameters at all. Do not append tracking tags, cache busters, or other extra parameters to a v2 URL; send only what the endpoint documents.
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "Invalid request",
|
||||
"details": [
|
||||
{ "code": "unrecognized_keys", "keys": ["limt"], "path": [], "message": "Unrecognized key: \"limt\"" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Use [Get Billing Status](/api-reference/billing/getBillingStatus) to inspect current credit and storage usage.
|
||||
</Callout>
|
||||
|
||||
@@ -171,6 +171,24 @@ The API uses standard HTTP status codes. v2 errors include a stable code and hum
|
||||
| `404` | Resource not found | Verify the ID exists and belongs to your workspace |
|
||||
| `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header |
|
||||
|
||||
### Unrecognized fields are rejected
|
||||
|
||||
Every v2 endpoint validates the request against its published schema — path parameters, query string, and body — and answers `400` for any field it does not declare. A misspelled parameter is an error rather than a silent no-op, so `?limt=20` fails instead of quietly returning an unbounded list.
|
||||
|
||||
This holds for endpoints that declare no query parameters at all. Do not append tracking tags, cache busters, or other extra parameters to a v2 URL; send only what the endpoint documents.
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "Invalid request",
|
||||
"details": [
|
||||
{ "code": "unrecognized_keys", "keys": ["limt"], "path": [], "message": "Unrecognized key: \"limt\"" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Use [Get Billing Status](/api-reference/billing/getBillingStatus) to inspect current credit and storage usage.
|
||||
</Callout>
|
||||
|
||||
@@ -171,6 +171,24 @@ The API uses standard HTTP status codes. v2 errors include a stable code and hum
|
||||
| `404` | Resource not found | Verify the ID exists and belongs to your workspace |
|
||||
| `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header |
|
||||
|
||||
### Unrecognized fields are rejected
|
||||
|
||||
Every v2 endpoint validates the request against its published schema — path parameters, query string, and body — and answers `400` for any field it does not declare. A misspelled parameter is an error rather than a silent no-op, so `?limt=20` fails instead of quietly returning an unbounded list.
|
||||
|
||||
This holds for endpoints that declare no query parameters at all. Do not append tracking tags, cache busters, or other extra parameters to a v2 URL; send only what the endpoint documents.
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "Invalid request",
|
||||
"details": [
|
||||
{ "code": "unrecognized_keys", "keys": ["limt"], "path": [], "message": "Unrecognized key: \"limt\"" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Use [Get Billing Status](/api-reference/billing/getBillingStatus) to inspect current credit and storage usage.
|
||||
</Callout>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"get": {
|
||||
"operationId": "getBillingStatus",
|
||||
"summary": "Get Billing Status",
|
||||
"description": "Return the current plan, billing standing, credit allowance, and storage quota. `credits` and `storage` report the payer's pooled allowances and are null unless the caller can manage that payer's billing; they are always null for a workspace API key. Billing history lives at `GET /api/v2/billing/logs`. Without a Stripe subscription — notably on the free plan — there is no real billing period: `period` is the open interval 1970-01-01 to 9999-12-31 and `credits.used` is lifetime consumption, not consumption since a period start.",
|
||||
"description": "Return the current plan, billing standing, credit allowance, and storage quota. `credits` and `storage` report the payer's pooled allowances and are null unless the caller can manage that payer's billing; they are always null for a workspace API key. Billing history lives at `GET /api/v2/billing/logs`.",
|
||||
"tags": ["Billing"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -101,7 +101,7 @@
|
||||
"get": {
|
||||
"operationId": "listBillingLogs",
|
||||
"summary": "List Billing Logs",
|
||||
"description": "List the credit-denominated billing ledger with source filtering and opaque cursor pagination. `period` defaults to `30d`, so an unqualified request covers only the last 30 days: paginating to `nextCursor: null` exhausts that window, not the whole ledger. Pass `period=all` for full history, or `period=custom` with `startDate` and `endDate` for a specific range.",
|
||||
"description": "List the credit-denominated billing ledger with source filtering and opaque cursor pagination. `period` defaults to `30d`, so an unqualified request covers only the last 30 days: paginating to `nextCursor: null` exhausts that window, not the whole ledger. An inverted custom window is a 400 rather than an empty page.",
|
||||
"tags": ["Billing"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -140,10 +140,10 @@
|
||||
"name": "period",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Relative window, all history, or a custom date range.",
|
||||
"description": "Relative window, all history, or a custom date range. `startDate` and `endDate` are accepted only with `custom`; every other value computes its own window.",
|
||||
"schema": {
|
||||
"default": "30d",
|
||||
"description": "Relative window, all history, or a custom date range.",
|
||||
"description": "Relative window, all history, or a custom date range. `startDate` and `endDate` are accepted only with `custom`; every other value computes its own window.",
|
||||
"type": "string",
|
||||
"enum": ["1d", "7d", "30d", "all", "custom"]
|
||||
}
|
||||
@@ -152,22 +152,24 @@
|
||||
"name": "startDate",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Start of a custom window as a Date-parseable string.",
|
||||
"description": "Only include usage events recorded at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. Requires `period=custom`. 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": {
|
||||
"description": "Start of a custom window as a Date-parseable string.",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"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 usage events recorded at or after this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. Requires `period=custom`. 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": "End of a custom window as a Date-parseable string; defaults to now.",
|
||||
"description": "Only include usage events recorded at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. Requires `period=custom`, and defaults to now when omitted. 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": {
|
||||
"description": "End of a custom window as a Date-parseable string; defaults to now.",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"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 usage events recorded at or before this UTC ISO 8601 timestamp, e.g. `2026-08-06T00:00:00Z`. Requires `period=custom`, and defaults to now when omitted. 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."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -187,9 +189,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -248,7 +250,7 @@
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings > API Keys. Operations that reject workspace keys say so in their own description."
|
||||
"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": {
|
||||
@@ -283,13 +285,13 @@
|
||||
}
|
||||
},
|
||||
"Retry-After": {
|
||||
"description": "Seconds to wait before retrying. Sent on `429` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset.",
|
||||
"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` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset."
|
||||
"description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset."
|
||||
}
|
||||
},
|
||||
"X-Run-Id": {
|
||||
@@ -304,7 +306,7 @@
|
||||
},
|
||||
"responses": {
|
||||
"BadRequest": {
|
||||
"description": "The request is invalid.",
|
||||
"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": {
|
||||
@@ -334,7 +336,7 @@
|
||||
}
|
||||
},
|
||||
"Forbidden": {
|
||||
"description": "The caller lacks the rights this operation requires. Where the cause is one a caller can act on, `error.details.code` names it, drawn 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\nA resource in a workspace the caller cannot reach at all answers `404`, not `403`, so absence and denial are indistinguishable to a caller who was never entitled to tell them apart.",
|
||||
"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": {
|
||||
@@ -364,7 +366,7 @@
|
||||
}
|
||||
},
|
||||
"RunIdConflict": {
|
||||
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
|
||||
"description": "The run cannot be started, for one of two causes named by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already claimed, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has reached the maximum workflow-to-workflow call depth.",
|
||||
"headers": {
|
||||
"X-Run-Id": {
|
||||
"$ref": "#/components/headers/X-Run-Id"
|
||||
@@ -378,18 +380,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Gone": {
|
||||
"description": "The requested generated resource has expired.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/V2Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayloadTooLarge": {
|
||||
"description": "The request, or a resource collection it must materialize, exceeds the allowed size. Besides an oversized request body, this covers a generated artifact that renders past the download ceiling and a workspace folder tree too large to load in full.",
|
||||
"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": {
|
||||
@@ -434,7 +426,7 @@
|
||||
}
|
||||
},
|
||||
"ClientClosedRequest": {
|
||||
"description": "The client closed the connection before the response was produced.",
|
||||
"description": "The client closed the connection before the response was produced. An abort can leave the run going, so `error.details.runId` carries the run id — reconcile against the runs resource rather than starting another run.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -454,7 +446,7 @@
|
||||
}
|
||||
},
|
||||
"ServiceUnavailable": {
|
||||
"description": "A required service is temporarily unavailable. The condition is transient, so the response normally carries `Retry-After` with the number of seconds to wait; treat that value as a floor and add jitter before retrying. One case deliberately omits the header: when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, the run may already have started, so retrying could start and bill a second run. Reconcile against the returned run id instead of retrying.",
|
||||
"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"
|
||||
@@ -485,7 +477,7 @@
|
||||
"description": "Human-readable explanation of the error."
|
||||
},
|
||||
"details": {
|
||||
"description": "Optional structured 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address."
|
||||
}
|
||||
},
|
||||
"required": ["code", "message"],
|
||||
@@ -754,7 +746,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Sim API v2 — Files & Audit Logs",
|
||||
"description": "Version 2 of the Sim REST API for workspace files and organization audit logs. Lists use opaque cursors, and rate-limit state is returned in response headers. Download File streams raw bytes as `application/octet-stream`; every other response uses the canonical v2 data, cursor-list, or error envelope.",
|
||||
"description": "Version 2 of the Sim REST API for workspace files, resumable uploads, public shares, and organization audit logs.",
|
||||
"version": "2.0.0",
|
||||
"contact": {
|
||||
"name": "Sim Support",
|
||||
@@ -40,7 +40,7 @@
|
||||
"get": {
|
||||
"operationId": "listFiles",
|
||||
"summary": "List Files",
|
||||
"description": "List workspace files with search, sorting, folder filtering, and opaque cursor pagination. Defaults to active files; pass `scope=archived` to page over soft-deleted files, whose `deletedAt` is non-null and which `POST /files/{fileId}/restore` can bring back.",
|
||||
"description": "List workspace files with search, sorting, folder filtering, and opaque cursor pagination. Defaults to active files; pass `scope=archived` to page over soft-deleted ones. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Files"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -58,20 +58,20 @@
|
||||
"name": "folderPath",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Restrict results to files directly inside this folder.",
|
||||
"description": "Restrict results to files directly inside this folder. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
|
||||
"schema": {
|
||||
"description": "Restrict results to files directly inside this folder.",
|
||||
"type": "string"
|
||||
"description": "Restrict results to files directly inside this folder. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "scope",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Which lifecycle set to list: `active` (default) for live files, `archived` for files a `DELETE` soft-deleted and that `POST /files/{fileId}/restore` can bring back. `folderPath` resolves against active folders only, so combining it with `scope=archived` returns 404 when the containing folder was archived too.",
|
||||
"description": "Which lifecycle set to list: `active` (default) for live files, `archived` for files a `DELETE` soft-deleted. `folderPath` resolves against active folders only, so pairing it with `scope=archived` returns an empty page when the containing folder was archived too.",
|
||||
"schema": {
|
||||
"default": "active",
|
||||
"description": "Which lifecycle set to list: `active` (default) for live files, `archived` for files a `DELETE` soft-deleted and that `POST /files/{fileId}/restore` can bring back. `folderPath` resolves against active folders only, so combining it with `scope=archived` returns 404 when the containing folder was archived too.",
|
||||
"description": "Which lifecycle set to list: `active` (default) for live files, `archived` for files a `DELETE` soft-deleted. `folderPath` resolves against active folders only, so pairing it with `scope=archived` returns an empty page when the containing folder was archived too.",
|
||||
"type": "string",
|
||||
"enum": ["active", "archived"]
|
||||
}
|
||||
@@ -92,10 +92,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "uploadedAt",
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": ["name", "size", "uploadedAt", "updatedAt"]
|
||||
}
|
||||
@@ -120,8 +120,6 @@
|
||||
"schema": {
|
||||
"description": "Maximum files per page. Values outside 1–1000 are truncated and clamped into that range rather than rejected. Defaults to 100.",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"default": 100
|
||||
}
|
||||
},
|
||||
@@ -129,9 +127,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -171,6 +169,9 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -301,6 +302,9 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -492,6 +496,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -598,7 +605,7 @@
|
||||
"get": {
|
||||
"operationId": "downloadFile",
|
||||
"summary": "Download File",
|
||||
"description": "Download the current file bytes from a workspace. A generated document is served as its compiled artifact, so it returns `409` while that artifact is still compiling and `413` if it renders past the size ceiling.",
|
||||
"description": "Download the current file bytes from a workspace. A generated document is served as its compiled artifact, so it answers `409` while that artifact is still compiling and `413` if it renders past the size ceiling. Downloading records an audit event, so it is not a safe read. A `HEAD` skips the effect but is authorized exactly as the `GET` is, so it answers `400`, `401`, `403`, or `404` wherever the `GET` would and an empty `200` otherwise. Skipping the effect means skipping the read that produces the payload, so that `200` carries none of the response headers documented below — it answers whether the `GET` would be allowed, not what the `GET` would return. In particular a `HEAD` does not report `Content-Length`, so it cannot be used to size a download in advance; read the size from the file resource instead.",
|
||||
"tags": ["Files"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -690,7 +697,7 @@
|
||||
"delete": {
|
||||
"operationId": "deleteFile",
|
||||
"summary": "Delete File",
|
||||
"description": "Archive a workspace file. This is a soft delete: the row is retained with a deletion timestamp, the file stops appearing in the default listing and is no longer readable through the API, and its stored bytes are never removed. List archived files with `GET /files?scope=archived` and reverse the delete with `POST /files/{fileId}/restore`.",
|
||||
"description": "Archive a workspace file. This is a soft delete: the file stops appearing in the default listing and is no longer readable through the API, but its stored bytes are never removed. Archiving an already-archived file is a `404`, not a no-op. List archived files with `GET /files?scope=archived`, and reverse the delete with `POST /files/{fileId}/restore`.",
|
||||
"tags": ["Files"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -752,9 +759,6 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -834,6 +838,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -850,7 +857,7 @@
|
||||
"post": {
|
||||
"operationId": "restoreFile",
|
||||
"summary": "Restore File",
|
||||
"description": "Reverse a soft delete and return the file to the workspace. Restore is not a pure undo: the file comes back at the workspace root regardless of the folder it was deleted from, and it gains a `_restored` suffix when another file at the root already holds its name — so read `folderPath` and `name` off the response rather than assuming the pre-delete values. Restoring a file that is already active is a no-op that returns that file, so a retry is safe. Returns 400 when the workspace itself has been archived, and 409 when no free restore name could be found.",
|
||||
"description": "Reverse a soft delete and return the file to the workspace. Not a pure undo: the file comes back at the workspace root, and gains a `_restored` suffix when another file there already holds its name, so read `folderPath` and `name` off the response. Restoring an already-active file returns it unchanged, so a retry is safe. An archived workspace is a `400`, and a name the restore could not free is a `409`.",
|
||||
"tags": ["Files"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -915,6 +922,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1009,7 +1019,7 @@
|
||||
"get": {
|
||||
"operationId": "listAuditLogs",
|
||||
"summary": "List Audit Logs",
|
||||
"description": "List an organization audit trail with filters and opaque cursor pagination. Requires an Enterprise subscription and organization admin or owner access. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key.",
|
||||
"description": "List an organization audit trail with filters and opaque cursor pagination. Requires an Enterprise subscription and organization admin or owner access. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Audit Logs"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1049,27 +1059,32 @@
|
||||
"description": "Filter to actions in one workspace.",
|
||||
"schema": {
|
||||
"description": "Filter to actions in one workspace.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "startDate",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Inclusive ISO 8601 start timestamp.",
|
||||
"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": {
|
||||
"description": "Inclusive ISO 8601 start timestamp.",
|
||||
"type": "string"
|
||||
"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": "Inclusive ISO 8601 end timestamp.",
|
||||
"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": {
|
||||
"description": "Inclusive ISO 8601 end timestamp.",
|
||||
"type": "string"
|
||||
"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."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1099,9 +1114,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -1161,9 +1176,6 @@
|
||||
"403": {
|
||||
"$ref": "#/components/responses/Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1180,7 +1192,7 @@
|
||||
"get": {
|
||||
"operationId": "getAuditLog",
|
||||
"summary": "Get Audit Log",
|
||||
"description": "Return one organization audit-log entry. Requires an Enterprise subscription and organization admin or owner access. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key.",
|
||||
"description": "Return one organization audit-log entry. Requires an Enterprise subscription and organization admin or owner access. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Audit Logs"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1306,6 +1318,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1398,7 +1413,7 @@
|
||||
"patch": {
|
||||
"operationId": "upsertFileShare",
|
||||
"summary": "Enable or Disable File Share",
|
||||
"description": "Create or partially update a server-tokenized public share. Only isActive is required, and an omitted authType keeps the stored auth mode. What happens to password and allowedEmails depends on the resulting mode, because enabling a share always rewrites the credentials the chosen mode does not use: 'public' clears the stored password and empties allowedEmails; 'password' keeps the stored password when password is omitted but empties allowedEmails; 'email' and 'sso' clear the stored password and keep the stored allowedEmails when the field is omitted. Only disabling with isActive false preserves the whole access configuration untouched — it also retains the token, so re-enabling restores the share as it was. Two enabling combinations are rejected outright with a 400 instead of being partially applied: 'password' when neither a password is supplied nor one is already stored, and 'email' or 'sso' when the resulting allowedEmails would be empty because none was supplied and none is stored. On a file that has never been shared there is nothing stored to fall back on, so enabling any mode other than 'public' must carry its credential in the same request. A workspace API key cannot call this operation. Because unauthorized resources are concealed, the rejection is reported as `404` rather than `403`; use a personal API key.",
|
||||
"description": "Create or partially update a server-tokenized public share. Only `isActive` is required; each other field states what enabling a mode does to it. Enabling any mode other than `public` on a file that has never been shared must carry its credential in the same request. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Files"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1460,6 +1475,9 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1607,6 +1625,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1623,7 +1644,7 @@
|
||||
"get": {
|
||||
"operationId": "listFilesFolders",
|
||||
"summary": "List Folders",
|
||||
"description": "List workspace file folders with optional parent-path filtering and sorting. The bounded set is returned in one page with `nextCursor` always null; there is no second page to fetch.",
|
||||
"description": "List workspace file folders with optional parent-path filtering and sorting. The bounded set is returned in one page; `nextCursor` is always null.",
|
||||
"tags": ["Files"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1644,7 +1665,7 @@
|
||||
"description": "Restrict results to direct children of this parent path.",
|
||||
"schema": {
|
||||
"description": "Restrict results to direct children of this parent path.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1663,10 +1684,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "name",
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": ["name", "createdAt", "updatedAt"]
|
||||
}
|
||||
@@ -1782,6 +1803,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1846,6 +1870,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1881,16 +1908,30 @@
|
||||
"description": "Path of the folder to delete.",
|
||||
"schema": {
|
||||
"description": "Path of the folder to delete.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "recursive",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Delete nested files and folders when true.",
|
||||
"description": "Delete the folder's nested files and folders too. An empty folder deletes either way; a non-empty one needs this. The listed spellings are the whole accepted vocabulary and are case-sensitive; any other value is rejected.",
|
||||
"schema": {
|
||||
"description": "Delete nested files and folders when true.",
|
||||
"description": "Delete the folder's nested files and folders too. An empty folder deletes either way; a non-empty one needs this. The listed spellings are the whole accepted vocabulary and are case-sensitive; any other value is rejected.",
|
||||
"enum": [
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
"on",
|
||||
"y",
|
||||
"enabled",
|
||||
"false",
|
||||
"0",
|
||||
"no",
|
||||
"off",
|
||||
"n",
|
||||
"disabled"
|
||||
],
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1952,7 +1993,7 @@
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings > API Keys. Operations that reject workspace keys say so in their own description."
|
||||
"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": {
|
||||
@@ -2012,13 +2053,13 @@
|
||||
}
|
||||
},
|
||||
"Retry-After": {
|
||||
"description": "Seconds to wait before retrying. Sent on `429` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset.",
|
||||
"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` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset."
|
||||
"description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset."
|
||||
}
|
||||
},
|
||||
"X-Run-Id": {
|
||||
@@ -2033,7 +2074,7 @@
|
||||
},
|
||||
"responses": {
|
||||
"BadRequest": {
|
||||
"description": "The request is invalid.",
|
||||
"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": {
|
||||
@@ -2063,7 +2104,7 @@
|
||||
}
|
||||
},
|
||||
"Forbidden": {
|
||||
"description": "The caller lacks the rights this operation requires. Where the cause is one a caller can act on, `error.details.code` names it, drawn 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\nA resource in a workspace the caller cannot reach at all answers `404`, not `403`, so absence and denial are indistinguishable to a caller who was never entitled to tell them apart.",
|
||||
"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": {
|
||||
@@ -2093,7 +2134,7 @@
|
||||
}
|
||||
},
|
||||
"RunIdConflict": {
|
||||
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
|
||||
"description": "The run cannot be started, for one of two causes named by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already claimed, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has reached the maximum workflow-to-workflow call depth.",
|
||||
"headers": {
|
||||
"X-Run-Id": {
|
||||
"$ref": "#/components/headers/X-Run-Id"
|
||||
@@ -2107,18 +2148,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Gone": {
|
||||
"description": "The requested generated resource has expired.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/V2Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayloadTooLarge": {
|
||||
"description": "The request, or a resource collection it must materialize, exceeds the allowed size. Besides an oversized request body, this covers a generated artifact that renders past the download ceiling and a workspace folder tree too large to load in full.",
|
||||
"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": {
|
||||
@@ -2163,7 +2194,7 @@
|
||||
}
|
||||
},
|
||||
"ClientClosedRequest": {
|
||||
"description": "The client closed the connection before the response was produced.",
|
||||
"description": "The client closed the connection before the response was produced. An abort can leave the run going, so `error.details.runId` carries the run id — reconcile against the runs resource rather than starting another run.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -2183,7 +2214,7 @@
|
||||
}
|
||||
},
|
||||
"ServiceUnavailable": {
|
||||
"description": "A required service is temporarily unavailable. The condition is transient, so the response normally carries `Retry-After` with the number of seconds to wait; treat that value as a floor and add jitter before retrying. One case deliberately omits the header: when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, the run may already have started, so retrying could start and bill a second run. Reconcile against the returned run id instead of retrying.",
|
||||
"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"
|
||||
@@ -2214,7 +2245,7 @@
|
||||
"description": "Human-readable explanation of the error."
|
||||
},
|
||||
"details": {
|
||||
"description": "Optional structured 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address."
|
||||
}
|
||||
},
|
||||
"required": ["code", "message"],
|
||||
@@ -2235,6 +2266,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"FolderPathInput": {
|
||||
"title": "Folder path input",
|
||||
"description": "Folder path. A missing leading slash is normalized before validation. 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": "string"
|
||||
},
|
||||
"V2File": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2251,12 +2288,12 @@
|
||||
"size": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"description": "Size in bytes of the stored file. For a generated document (docx, pptx, pdf, xlsx) the stored file is the generation source rather than the rendered document, so this does not predict how many bytes `GET /files/{fileId}` returns — that endpoint serves the compiled artifact, which is typically much larger.",
|
||||
"description": "Size in bytes of the stored file. For a generated document (docx, pptx, pdf, xlsx) this is the generation source, not the rendered document, so it does not predict how many bytes `GET /files/{fileId}` returns.",
|
||||
"examples": [1024]
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "MIME type of the stored file. For a generated document (docx, pptx, pdf, xlsx) the stored file is the generation source, so this describes the source and not what `GET /files/{fileId}` serves — that endpoint returns the compiled artifact under the rendered document type.",
|
||||
"description": "MIME type of the stored file. For a generated document (docx, pptx, pdf, xlsx) this is the generation source type, not the rendered document type `GET /files/{fileId}` serves.",
|
||||
"examples": ["text/csv"]
|
||||
},
|
||||
"key": {
|
||||
@@ -2266,7 +2303,9 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"type": "string",
|
||||
"description": "Canonical containing-folder path. `/` is the workspace root."
|
||||
"title": "Folder path",
|
||||
"description": "Canonical containing-folder path. `/` is the workspace root.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"uploadedByEmail": {
|
||||
"type": "string",
|
||||
@@ -2336,7 +2375,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -2414,11 +2453,11 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"description": "Canonical containing-folder path. Omit for the workspace root.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
},
|
||||
"content": {
|
||||
"default": "",
|
||||
"description": "Initial file content. Omit or send an empty string for a zero-byte file. The 70,000,000-character bound is a JSON-envelope guard, not the file-size limit: the decoded bytes must be at most 50 MiB, so a longer base64 payload is admitted here and then rejected with 413. Use an upload session for anything larger.",
|
||||
"description": "Initial file content. Omit or send an empty string for a zero-byte file. The 70,000,000-character bound guards the JSON envelope; the decoded bytes must be at most 50 MiB, and a longer base64 payload is rejected with `413`. Use an upload session for anything larger.",
|
||||
"type": "string",
|
||||
"maxLength": 70000000
|
||||
},
|
||||
@@ -2514,7 +2553,7 @@
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Signed URL to which the file bytes are uploaded."
|
||||
"description": "Signed URL to which the file bytes are uploaded. Send the bytes with `PUT` to this URL, including exactly the headers in `headers` and nothing that alters the body. Success is `204` with an empty body. A failure is the same `{ \"error\": { \"code\", \"message\" } }` envelope as every other v2 response: `400` when the body does not match the size or content type the session was created for, `403` when the token is invalid, expired, or belongs to another session, and `409` when the session is no longer accepting bytes. The URL is signed and self-describing — construct it from this field only, never by hand."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
@@ -2634,7 +2673,7 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"description": "Canonical destination folder path. Omit for the workspace root.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "name", "contentType", "size"],
|
||||
@@ -2667,7 +2706,7 @@
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Signed URL for this upload part."
|
||||
"description": "Signed URL for this upload part. Send the bytes with `PUT` to this URL, including exactly the headers in `headers` and nothing that alters the body. Success is `204` with an empty body. A failure is the same `{ \"error\": { \"code\", \"message\" } }` envelope as every other v2 response: `400` when the body does not match the size or content type the session was created for, `403` when the token is invalid, expired, or belongs to another session, and `409` when the session is no longer accepting bytes. The URL is signed and self-describing — construct it from this field only, never by hand."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
@@ -2931,12 +2970,12 @@
|
||||
"size": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"description": "Size in bytes of the stored file. For a generated document (docx, pptx, pdf, xlsx) the stored file is the generation source rather than the rendered document, so this does not predict how many bytes `GET /files/{fileId}` returns — that endpoint serves the compiled artifact, which is typically much larger.",
|
||||
"description": "Size in bytes of the stored file. For a generated document (docx, pptx, pdf, xlsx) this is the generation source, not the rendered document, so it does not predict how many bytes `GET /files/{fileId}` returns.",
|
||||
"examples": [1024]
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "MIME type of the stored file. For a generated document (docx, pptx, pdf, xlsx) the stored file is the generation source, so this describes the source and not what `GET /files/{fileId}` serves — that endpoint returns the compiled artifact under the rendered document type.",
|
||||
"description": "MIME type of the stored file. For a generated document (docx, pptx, pdf, xlsx) this is the generation source type, not the rendered document type `GET /files/{fileId}` serves.",
|
||||
"examples": ["text/csv"]
|
||||
},
|
||||
"key": {
|
||||
@@ -2946,7 +2985,9 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"type": "string",
|
||||
"description": "Canonical containing-folder path. `/` is the workspace root."
|
||||
"title": "Folder path",
|
||||
"description": "Canonical containing-folder path. `/` is the workspace root.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"uploadedByEmail": {
|
||||
"type": "string",
|
||||
@@ -3196,7 +3237,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -3325,7 +3366,7 @@
|
||||
},
|
||||
"targetFolderPath": {
|
||||
"description": "Destination folder path. Omit to move files to the workspace root.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "fileIds"],
|
||||
@@ -3409,21 +3450,21 @@
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the share should resolve."
|
||||
"description": "Whether the share should resolve. Disabling preserves the token and the whole access configuration, so re-enabling restores the share as it was; enabling rewrites the credentials the resulting mode does not use."
|
||||
},
|
||||
"authType": {
|
||||
"description": "How access to the share is gated.",
|
||||
"description": "How access to the share is gated. The stored mode is kept when omitted. Enabling `public` clears the stored password and empties `allowedEmails`; `password` empties `allowedEmails`; `email` and `sso` clear the stored password.",
|
||||
"type": "string",
|
||||
"enum": ["public", "password", "email", "sso"]
|
||||
},
|
||||
"password": {
|
||||
"description": "Password for a password-gated share.",
|
||||
"description": "Password for a password-gated share. Kept when omitted; enabling `password` with neither a supplied nor a stored password is a 400.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 1024
|
||||
},
|
||||
"allowedEmails": {
|
||||
"description": "Allowed addresses or @domain patterns for email and SSO shares.",
|
||||
"description": "Allowed addresses or `@domain` patterns for email and SSO shares. Kept when omitted; enabling `email` or `sso` with an empty resulting list is a 400.",
|
||||
"maxItems": 200,
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -3460,7 +3501,7 @@
|
||||
"content": {
|
||||
"type": "string",
|
||||
"maxLength": 70000000,
|
||||
"description": "Complete replacement content for the file. The 70,000,000-character bound is a JSON-envelope guard, not the file-size limit: the decoded bytes must be at most 50 MiB, so a longer base64 payload is admitted here and then rejected with 413."
|
||||
"description": "Complete replacement content for the file. The 70,000,000-character bound guards the JSON envelope; the decoded bytes must be at most 50 MiB, and a longer base64 payload is rejected with `413`."
|
||||
},
|
||||
"encoding": {
|
||||
"default": "utf-8",
|
||||
@@ -3564,11 +3605,15 @@
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Canonical folder path used as the public folder identifier."
|
||||
"title": "Non-root folder path",
|
||||
"description": "Canonical folder path used as the public folder identifier.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"parentPath": {
|
||||
"type": "string",
|
||||
"description": "Canonical parent path; `/` is the root."
|
||||
"title": "Folder path",
|
||||
"description": "Canonical parent path; `/` is the root.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
@@ -3605,7 +3650,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
|
||||
}
|
||||
},
|
||||
"required": ["data", "nextCursor"],
|
||||
@@ -3626,6 +3671,12 @@
|
||||
"title": "File folder response",
|
||||
"description": "A single workspace file folder."
|
||||
},
|
||||
"NonRootFolderPathInput": {
|
||||
"title": "Non-root folder path input",
|
||||
"description": "Non-root folder path. A missing leading slash is normalized before validation. 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": "string"
|
||||
},
|
||||
"CreateFileFolderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3636,7 +3687,7 @@
|
||||
},
|
||||
"path": {
|
||||
"description": "Path of the folder to create.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "path"],
|
||||
@@ -3654,11 +3705,11 @@
|
||||
},
|
||||
"path": {
|
||||
"description": "Current folder path.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
},
|
||||
"destinationPath": {
|
||||
"description": "New full path for the folder and its descendants.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "path", "destinationPath"],
|
||||
@@ -3671,7 +3722,9 @@
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Deleted folder path."
|
||||
"title": "Folder path",
|
||||
"description": "Deleted folder path.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"deleted": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"get": {
|
||||
"operationId": "listKnowledgeBases",
|
||||
"summary": "List Knowledge Bases",
|
||||
"description": "List knowledge bases in a workspace with folder filtering, search, and sorting. Paginate with `limit` and `cursor`, stopping when `nextCursor` is null. An unknown `folderPath` is a 404. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "List knowledge bases in a workspace with folder filtering, search, sorting, and opaque cursor pagination. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -54,19 +54,19 @@
|
||||
"name": "folderPath",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Restrict results to knowledge bases in this folder.",
|
||||
"description": "Restrict results to knowledge bases in this folder. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
|
||||
"schema": {
|
||||
"description": "Restrict results to knowledge bases in this folder.",
|
||||
"type": "string"
|
||||
"description": "Restrict results to knowledge bases in this folder. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Case-insensitive substring search on the resource name.",
|
||||
"description": "Case-insensitive substring match against the resource name.",
|
||||
"schema": {
|
||||
"description": "Case-insensitive substring search on the resource name.",
|
||||
"description": "Case-insensitive substring match against the resource name.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 200
|
||||
@@ -76,10 +76,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "createdAt",
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": ["name", "createdAt", "updatedAt"]
|
||||
}
|
||||
@@ -113,9 +113,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -172,7 +172,7 @@
|
||||
"post": {
|
||||
"operationId": "createKnowledgeBase",
|
||||
"summary": "Create Knowledge Base",
|
||||
"description": "Create a knowledge base in a workspace with optional folder placement and chunking configuration. An unknown `folderPath` is a 404. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "Create a knowledge base in a workspace with optional folder placement and chunking configuration. An unknown `folderPath` is a `404`. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -241,7 +241,7 @@
|
||||
"get": {
|
||||
"operationId": "getKnowledgeBase",
|
||||
"summary": "Get Knowledge Base",
|
||||
"description": "Retrieve a knowledge base by identifier. Inaccessible knowledge bases are reported as not found. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "Retrieve a knowledge base by identifier. Inaccessible knowledge bases are reported as not found. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -318,7 +318,7 @@
|
||||
"patch": {
|
||||
"operationId": "updateKnowledgeBase",
|
||||
"summary": "Update Knowledge Base",
|
||||
"description": "Update a knowledge base name, description, chunking configuration, or folder placement. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "Update a knowledge base name, description, chunking configuration, or folder placement. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -474,7 +474,7 @@
|
||||
"post": {
|
||||
"operationId": "searchKnowledge",
|
||||
"summary": "Search Knowledge",
|
||||
"description": "Search one or more knowledge bases with semantic vector retrieval, optional hybrid full-text retrieval, and structured tag filters. Set `rerankerEnabled` with a `rerankerModel` to re-order the retrieved chunks with a reranking model before truncating to `topK`; reranked results carry a `rerankerScore` and are ordered by it, and reranking is billed as an additional search unit. Every result names the `knowledgeBaseId` it came from. The request body is capped at 2 MiB; a larger body is a 413.",
|
||||
"description": "Search one or more knowledge bases with semantic vector retrieval, optional hybrid full-text retrieval, and structured tag filters. Every result names the `knowledgeBaseId` it came from. A request body over 2 MiB is a `413`.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -543,7 +543,7 @@
|
||||
"get": {
|
||||
"operationId": "listKnowledgeTags",
|
||||
"summary": "List Tags",
|
||||
"description": "List the knowledge base's tag vocabulary: each tag's display name, the slot it is stored in, and its field type. Display names are what tag filters and the tag values on document reads use; slots are what document writes set. Every slot listed here is writable, in its declared type: `tag1`..`tag7` take a string, `number1`..`number5` a number, `date1`..`date2` a `YYYY-MM-DD` string, and `boolean1`..`boolean3` a boolean. The vocabulary is bounded by the fixed slot table. The bounded set is returned in one page with `nextCursor` always null; there is no second page to fetch.",
|
||||
"description": "List the knowledge base's tag vocabulary: each tag's display name, the slot it is stored in, and its field type. Filters and document reads use display names; document writes address slots. The bounded set is returned in one page; `nextCursor` is always null.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -619,7 +619,7 @@
|
||||
"get": {
|
||||
"operationId": "listKnowledgeDocuments",
|
||||
"summary": "List Documents",
|
||||
"description": "List documents in a knowledge base with filename search, state filtering, tag filtering, sorting, and opaque cursor pagination. Each document carries its tag values keyed by tag display name; resolve those names to write slots with `GET /api/v2/knowledge/{id}/tags`.",
|
||||
"description": "List documents in a knowledge base with filename search, state filtering, tag filtering, sorting, and opaque cursor pagination. Tag values are keyed by display name; resolve those to write slots with `GET /api/v2/knowledge/{id}/tags`.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -648,10 +648,10 @@
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Maximum documents to return, between 1 and 100.",
|
||||
"description": "Maximum documents to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
|
||||
"schema": {
|
||||
"default": 50,
|
||||
"description": "Maximum documents to return, between 1 and 100.",
|
||||
"description": "Maximum documents to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
@@ -661,10 +661,12 @@
|
||||
"name": "search",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Case-insensitive filename search.",
|
||||
"description": "Case-insensitive substring match against the document filename.",
|
||||
"schema": {
|
||||
"description": "Case-insensitive filename search.",
|
||||
"type": "string"
|
||||
"description": "Case-insensitive substring match against the document filename.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -683,10 +685,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Document field used to sort results.",
|
||||
"description": "Field used to sort the result. Sorting by `filename` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "uploadedAt",
|
||||
"description": "Document field used to sort results.",
|
||||
"description": "Field used to sort the result. Sorting by `filename` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"filename",
|
||||
@@ -715,9 +717,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -784,7 +786,7 @@
|
||||
"patch": {
|
||||
"operationId": "bulkUpdateKnowledgeDocuments",
|
||||
"summary": "Bulk Enable or Disable Documents",
|
||||
"description": "Enable or disable many documents in one request, either by identifier (up to 100) or, with `selectAll`, every document in the knowledge base optionally narrowed by `enabledFilter`. Disabling keeps a document indexed but excludes it from search. Bulk delete is deliberately not offered: the bulk path records no audit entries, so deletions go through `DELETE /api/v2/knowledge/{id}/documents/{documentId}`, which audits each one. An identifier request echoes the documents it changed in `documentIds`; a `selectAll` request omits that field because the selection is unbounded, and reports `updatedCount` alone. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key.",
|
||||
"description": "Enable or disable many documents in one request, either by identifier or, with `selectAll`, every document in the knowledge base. Bulk delete is not offered; delete documents one at a time with `DELETE /api/v2/knowledge/{id}/documents/{documentId}`. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -844,6 +846,9 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1236,6 +1241,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1441,7 +1449,7 @@
|
||||
"patch": {
|
||||
"operationId": "updateKnowledgeDocument",
|
||||
"summary": "Update Document",
|
||||
"description": "Rename a document, enable or disable it for search, set any of its 17 tag slots, or requeue it for processing. A tag slot takes its declared type — a string for `tag1`..`tag7`, a number for `number1`..`number5`, a `YYYY-MM-DD` string for `date1`..`date2`, a boolean for `boolean1`..`boolean3` — and a value that is not valid for the slot is a `400` rather than a silently cleared tag. Resolve a display name to its slot with `GET /api/v2/knowledge/{id}/tags`. Absent fields are unchanged. Only caller-owned fields are accepted: derived indexing state (`chunkCount`, `tokenCount`, `characterCount`, `processingStatus`, `processingError`) is written by the processing pipeline and cannot be asserted here. `retryProcessing: true` re-queues a failed or stuck document and must be sent on its own — it runs instead of, not alongside, the field updates — and it answers with a queue acknowledgement rather than the document. Otherwise the updated document is returned; it omits the connector provenance the detail read carries, so re-read with GET when that is needed. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key.",
|
||||
"description": "Rename a document, enable or disable it for search, set any of its 17 tag slots, or requeue it for processing. Absent fields are unchanged, and derived indexing state is read-only. Resolve a tag display name to its slot with `GET /api/v2/knowledge/{id}/tags`. The returned document omits the connector provenance the detail read carries. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1512,6 +1520,9 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1526,7 +1537,7 @@
|
||||
"delete": {
|
||||
"operationId": "deleteKnowledgeDocument",
|
||||
"summary": "Delete Document",
|
||||
"description": "Remove one document from a knowledge base. What that means depends on the document. A directly uploaded document is deleted outright along with its indexed chunks. A connector-backed document is instead excluded: its row survives, marked excluded and disabled so it stops being searchable and a later connector sync does not re-add it, and its embeddings are not deleted. Either way the document no longer appears in listings or search results.",
|
||||
"description": "Remove one document from a knowledge base. An uploaded document is deleted outright with its indexed chunks. A connector-backed document is instead excluded — its row and embeddings survive, but it stops being searchable and a later sync does not re-add it. Either way it no longer appears in listings or search results.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1613,7 +1624,7 @@
|
||||
"get": {
|
||||
"operationId": "listKnowledgeFolders",
|
||||
"summary": "List Folders",
|
||||
"description": "List folders in the knowledge-base folder tree with filtering and sorting. The bounded set is returned in one page with `nextCursor` always null; there is no second page to fetch. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "List folders in the knowledge-base folder tree with filtering and sorting. The bounded set is returned in one page; `nextCursor` is always null. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1634,7 +1645,7 @@
|
||||
"description": "Restrict results to direct children of this parent path.",
|
||||
"schema": {
|
||||
"description": "Restrict results to direct children of this parent path.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1653,10 +1664,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "name",
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": ["name", "createdAt", "updatedAt"]
|
||||
}
|
||||
@@ -1725,7 +1736,7 @@
|
||||
"post": {
|
||||
"operationId": "createKnowledgeFolder",
|
||||
"summary": "Create Folder",
|
||||
"description": "Create a folder in the knowledge-base folder tree. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "Create a folder in the knowledge-base folder tree. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -1792,7 +1803,7 @@
|
||||
"patch": {
|
||||
"operationId": "relocateKnowledgeFolder",
|
||||
"summary": "Rename or Move Folder",
|
||||
"description": "Rename or move a folder and atomically rewrite descendant paths. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "Rename or move a folder and atomically rewrite descendant paths. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Knowledge Bases"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -1880,16 +1891,30 @@
|
||||
"description": "Path of the folder to delete.",
|
||||
"schema": {
|
||||
"description": "Path of the folder to delete.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "recursive",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Delete nested files and folders when true.",
|
||||
"description": "Delete the folder's nested files and folders too. An empty folder deletes either way; a non-empty one needs this. The listed spellings are the whole accepted vocabulary and are case-sensitive; any other value is rejected.",
|
||||
"schema": {
|
||||
"description": "Delete nested files and folders when true.",
|
||||
"description": "Delete the folder's nested files and folders too. An empty folder deletes either way; a non-empty one needs this. The listed spellings are the whole accepted vocabulary and are case-sensitive; any other value is rejected.",
|
||||
"enum": [
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
"on",
|
||||
"y",
|
||||
"enabled",
|
||||
"false",
|
||||
"0",
|
||||
"no",
|
||||
"off",
|
||||
"n",
|
||||
"disabled"
|
||||
],
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1954,7 +1979,7 @@
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings > API Keys. Operations that reject workspace keys say so in their own description."
|
||||
"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": {
|
||||
@@ -1989,13 +2014,13 @@
|
||||
}
|
||||
},
|
||||
"Retry-After": {
|
||||
"description": "Seconds to wait before retrying. Sent on `429` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset.",
|
||||
"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` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset."
|
||||
"description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset."
|
||||
}
|
||||
},
|
||||
"X-Run-Id": {
|
||||
@@ -2010,7 +2035,7 @@
|
||||
},
|
||||
"responses": {
|
||||
"BadRequest": {
|
||||
"description": "The request is invalid.",
|
||||
"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": {
|
||||
@@ -2040,7 +2065,7 @@
|
||||
}
|
||||
},
|
||||
"Forbidden": {
|
||||
"description": "The caller lacks the rights this operation requires. Where the cause is one a caller can act on, `error.details.code` names it, drawn 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\nA resource in a workspace the caller cannot reach at all answers `404`, not `403`, so absence and denial are indistinguishable to a caller who was never entitled to tell them apart.",
|
||||
"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": {
|
||||
@@ -2070,7 +2095,7 @@
|
||||
}
|
||||
},
|
||||
"RunIdConflict": {
|
||||
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
|
||||
"description": "The run cannot be started, for one of two causes named by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already claimed, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has reached the maximum workflow-to-workflow call depth.",
|
||||
"headers": {
|
||||
"X-Run-Id": {
|
||||
"$ref": "#/components/headers/X-Run-Id"
|
||||
@@ -2084,18 +2109,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Gone": {
|
||||
"description": "The requested generated resource has expired.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/V2Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayloadTooLarge": {
|
||||
"description": "The request, or a resource collection it must materialize, exceeds the allowed size. Besides an oversized request body, this covers a generated artifact that renders past the download ceiling and a workspace folder tree too large to load in full.",
|
||||
"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": {
|
||||
@@ -2140,7 +2155,7 @@
|
||||
}
|
||||
},
|
||||
"ClientClosedRequest": {
|
||||
"description": "The client closed the connection before the response was produced.",
|
||||
"description": "The client closed the connection before the response was produced. An abort can leave the run going, so `error.details.runId` carries the run id — reconcile against the runs resource rather than starting another run.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -2160,7 +2175,7 @@
|
||||
}
|
||||
},
|
||||
"ServiceUnavailable": {
|
||||
"description": "A required service is temporarily unavailable. The condition is transient, so the response normally carries `Retry-After` with the number of seconds to wait; treat that value as a floor and add jitter before retrying. One case deliberately omits the header: when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, the run may already have started, so retrying could start and bill a second run. Reconcile against the returned run id instead of retrying.",
|
||||
"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"
|
||||
@@ -2191,7 +2206,7 @@
|
||||
"description": "Human-readable explanation of the error."
|
||||
},
|
||||
"details": {
|
||||
"description": "Optional structured 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address."
|
||||
}
|
||||
},
|
||||
"required": ["code", "message"],
|
||||
@@ -2212,6 +2227,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"FolderPathInput": {
|
||||
"title": "Folder path input",
|
||||
"description": "Folder path. A missing leading slash is normalized before validation. 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": "string"
|
||||
},
|
||||
"V2KnowledgeBase": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2289,7 +2310,9 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"type": "string",
|
||||
"title": "Folder path",
|
||||
"description": "Canonical containing-folder path; `/` is the workspace root.",
|
||||
"maxLength": 4096,
|
||||
"examples": ["/Product"]
|
||||
}
|
||||
},
|
||||
@@ -2388,7 +2411,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -2472,7 +2495,7 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"description": "Containing folder path; omission creates the knowledge base at the root.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "name"],
|
||||
@@ -2507,7 +2530,7 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"description": "New containing-folder path.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId"],
|
||||
@@ -2672,9 +2695,22 @@
|
||||
"maximum": 9007199254740991,
|
||||
"description": "Number of results returned.",
|
||||
"examples": [4]
|
||||
},
|
||||
"rerankerStatus": {
|
||||
"type": "string",
|
||||
"enum": ["not_requested", "skipped", "unavailable", "applied"],
|
||||
"description": "What the reranker did on this search. `applied` means it ordered the results, which carry `rerankerScore`. `unavailable` means it was attempted but could not complete, so results are in vector order with no `rerankerScore` — the search still succeeded, and is worth retrying. `skipped` means there was nothing to rank. `not_requested` means `rerankerEnabled` was absent or false.",
|
||||
"examples": ["applied"]
|
||||
}
|
||||
},
|
||||
"required": ["results", "query", "knowledgeBaseIds", "topK", "totalResults"],
|
||||
"required": [
|
||||
"results",
|
||||
"query",
|
||||
"knowledgeBaseIds",
|
||||
"topK",
|
||||
"totalResults",
|
||||
"rerankerStatus"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"title": "Knowledge search data",
|
||||
"description": "Results and execution context for a knowledge search."
|
||||
@@ -2782,7 +2818,7 @@
|
||||
"maximum": 100
|
||||
},
|
||||
"tagFilters": {
|
||||
"description": "Structured tag filters. Supported across multiple knowledge bases, but each filtered tag must resolve to the same slot and field type in every knowledge base selected; a tag missing from one of them, or defined inconsistently across them, is rejected and those knowledge bases must be searched separately. A tag name defined in none of the selected knowledge bases is rejected, never ignored; list the available names with GET /api/v2/knowledge/{id}/tags.",
|
||||
"description": "Structured tag filters. Each filtered tag must resolve to the same slot and field type in every knowledge base selected; one missing from any of them, or defined inconsistently across them, is rejected rather than ignored, and those knowledge bases must be searched separately. List the available names with `GET /api/v2/knowledge/{id}/tags`.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/V2KnowledgeSearchTagFilter"
|
||||
@@ -2802,11 +2838,12 @@
|
||||
]
|
||||
},
|
||||
"rerankerEnabled": {
|
||||
"description": "Re-order retrieved chunks with a reranking model before truncating to `topK`. Ignored for a tag-only search, which has no query to rank against. Reranking is billed as an additional search unit.",
|
||||
"description": "Re-order retrieved chunks with a reranking model before truncating to `topK`. Ignored for a tag-only search, and billed as an additional search unit. Reranking is best-effort — a provider failure falls back to vector ordering, so check `rerankerStatus` on the response.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"rerankerModel": {
|
||||
"description": "Reranking model to use; required for reranking to run.",
|
||||
"default": "rerank-v4.0-fast",
|
||||
"description": "Reranking model to use when `rerankerEnabled` is true. Defaults to `rerank-v4.0-fast`.",
|
||||
"type": "string",
|
||||
"enum": ["rerank-v4.0-pro", "rerank-v4.0-fast", "rerank-v3.5"]
|
||||
},
|
||||
@@ -2818,6 +2855,7 @@
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "knowledgeBaseIds"],
|
||||
"additionalProperties": false,
|
||||
"title": "Search knowledge request",
|
||||
"description": "Knowledge bases, query, result limit, retrieval mode, and optional tag filters."
|
||||
},
|
||||
@@ -2864,7 +2902,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
|
||||
}
|
||||
},
|
||||
"required": ["data", "nextCursor"],
|
||||
@@ -3007,7 +3045,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -3324,7 +3362,7 @@
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Signed URL to which the file bytes are uploaded."
|
||||
"description": "Signed URL to which the file bytes are uploaded. Send the bytes with `PUT` to this URL, including exactly the headers in `headers` and nothing that alters the body. Success is `204` with an empty body. A failure is the same `{ \"error\": { \"code\", \"message\" } }` envelope as every other v2 response: `400` when the body does not match the size or content type the session was created for, `403` when the token is invalid, expired, or belongs to another session, and `409` when the session is no longer accepting bytes. The URL is signed and self-describing — construct it from this field only, never by hand."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
@@ -3527,7 +3565,7 @@
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Signed URL for this upload part."
|
||||
"description": "Signed URL for this upload part. Send the bytes with `PUT` to this URL, including exactly the headers in `headers` and nothing that alters the body. Success is `204` with an empty body. A failure is the same `{ \"error\": { \"code\", \"message\" } }` envelope as every other v2 response: `400` when the body does not match the size or content type the session was created for, `403` when the token is invalid, expired, or belongs to another session, and `409` when the session is no longer accepting bytes. The URL is signed and self-describing — construct it from this field only, never by hand."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
@@ -3955,7 +3993,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"retryProcessing": {
|
||||
"description": "Requeue the document for processing. Send it alone: no other field may accompany it.",
|
||||
"description": "Requeue a failed or stuck document for processing. Send it alone — no other field may accompany it — and it answers with a queue acknowledgement rather than the document.",
|
||||
"type": "boolean",
|
||||
"const": true
|
||||
}
|
||||
@@ -3981,11 +4019,15 @@
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Canonical folder path used as the public folder identifier."
|
||||
"title": "Non-root folder path",
|
||||
"description": "Canonical folder path used as the public folder identifier.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"parentPath": {
|
||||
"type": "string",
|
||||
"description": "Canonical parent path; `/` is the root."
|
||||
"title": "Folder path",
|
||||
"description": "Canonical parent path; `/` is the root.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
@@ -4022,13 +4064,13 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
|
||||
}
|
||||
},
|
||||
"required": ["data", "nextCursor"],
|
||||
"additionalProperties": false,
|
||||
"title": "Knowledge folder list response",
|
||||
"description": "A cursor-paginated page of knowledge-base folders."
|
||||
"description": "The whole bounded set of knowledge-base folders, in one page."
|
||||
},
|
||||
"V2KnowledgeFolderResponse": {
|
||||
"type": "object",
|
||||
@@ -4043,6 +4085,12 @@
|
||||
"title": "Knowledge folder response",
|
||||
"description": "A single knowledge-base folder."
|
||||
},
|
||||
"NonRootFolderPathInput": {
|
||||
"title": "Non-root folder path input",
|
||||
"description": "Non-root folder path. A missing leading slash is normalized before validation. 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": "string"
|
||||
},
|
||||
"CreateKnowledgeFolderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4053,7 +4101,7 @@
|
||||
},
|
||||
"path": {
|
||||
"description": "Path of the folder to create.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "path"],
|
||||
@@ -4071,11 +4119,11 @@
|
||||
},
|
||||
"path": {
|
||||
"description": "Current folder path.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
},
|
||||
"destinationPath": {
|
||||
"description": "New full path for the folder and its descendants.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "path", "destinationPath"],
|
||||
@@ -4088,7 +4136,9 @@
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Canonical path of the deleted folder."
|
||||
"title": "Folder path",
|
||||
"description": "Canonical path of the deleted folder.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"deleted": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"get": {
|
||||
"operationId": "listLogs",
|
||||
"summary": "List Logs",
|
||||
"description": "List workflow execution logs for a workspace with filters, selectable detail, and opaque cursor pagination. This list predates the shared sort convention: it has no `sortBy` (the sort column is fixed to execution start time) and spells the direction `order` rather than `sortOrder`. Trace spans are stored separately from the log row and are pruned on their own retention schedule: `includeTraceSpans=true` on a run whose stored spans have aged out returns `traceSpans: []` rather than an error, so an empty array does not mean the run recorded no spans.",
|
||||
"description": "List workflow execution logs for a workspace with filters, selectable detail, and opaque cursor pagination. 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": [
|
||||
{
|
||||
@@ -54,20 +54,20 @@
|
||||
"name": "workflowIds",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Comma-separated workflow identifiers to include.",
|
||||
"description": "Comma-separated workflow identifiers to include. An empty entry is rejected.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated workflow identifiers to include."
|
||||
"description": "Comma-separated workflow identifiers to include. An empty entry is rejected."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "triggers",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Comma-separated trigger types to include.",
|
||||
"description": "Comma-separated trigger types to include. An empty entry is rejected. 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`.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated trigger types to include."
|
||||
"description": "Comma-separated trigger types to include. An empty entry is rejected. 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`."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85,44 +85,48 @@
|
||||
"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.",
|
||||
"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."
|
||||
"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.",
|
||||
"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."
|
||||
"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.",
|
||||
"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": "number",
|
||||
"description": "Minimum total execution duration in milliseconds."
|
||||
"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.",
|
||||
"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": "number",
|
||||
"description": "Maximum total execution duration in milliseconds."
|
||||
"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."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -159,21 +163,21 @@
|
||||
"name": "details",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Response detail level.",
|
||||
"description": "Response detail level. `full` adds the `workflow` summary to every item. `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."
|
||||
"description": "Response detail level. `full` adds the `workflow` summary to every item. `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.",
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@@ -181,9 +185,9 @@
|
||||
"name": "includeFinalOutput",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Whether to include the final workflow output.",
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@@ -195,8 +199,6 @@
|
||||
"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",
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"default": 100
|
||||
}
|
||||
},
|
||||
@@ -204,9 +206,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -215,12 +217,12 @@
|
||||
"name": "order",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Sort direction by execution start time. This operation deviates from the v2 `sortBy` + `sortOrder` convention: logs are sortable only by start time, so the direction is carried by this single `order` param and `sortBy`/`sortOrder` are not accepted.",
|
||||
"description": "Sort direction by execution start time. This list is sortable only by execution start time, so it takes `order` in place of `sortBy`/`sortOrder`, which it rejects.",
|
||||
"schema": {
|
||||
"default": "desc",
|
||||
"description": "Sort direction by execution start time. This list is sortable only by execution start time, so it takes `order` in place of `sortBy`/`sortOrder`, which it rejects.",
|
||||
"type": "string",
|
||||
"enum": ["desc", "asc"],
|
||||
"description": "Sort direction by execution start time. This operation deviates from the v2 `sortBy` + `sortOrder` convention: logs are sortable only by start time, so the direction is carried by this single `order` param and `sortBy`/`sortOrder` are not accepted."
|
||||
"enum": ["asc", "desc"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -231,6 +233,8 @@
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 128,
|
||||
"pattern": "^[A-Za-z0-9._:-]+$",
|
||||
"description": "Exact run identifier to match."
|
||||
}
|
||||
},
|
||||
@@ -238,10 +242,10 @@
|
||||
"name": "folderPaths",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Comma-separated workflow folder paths to include.",
|
||||
"description": "Comma-separated workflow folder paths to include. 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."
|
||||
"description": "Comma-separated workflow folder paths to include. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error."
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -295,18 +299,20 @@
|
||||
"get": {
|
||||
"operationId": "getLog",
|
||||
"summary": "Get Log",
|
||||
"description": "Retrieve the diagnostic representation of a run, including its workflow snapshot, trace spans, final output, and cost. The returned `workflowState` snapshot has credential values redacted: OAuth credential references and secret (`password`) sub-block values are null, while `{{VAR}}` environment-variable references are preserved so consecutive snapshots stay diffable. Trace spans are stored separately from the log row and are pruned on their own retention schedule: a run whose stored spans have aged out returns `traceSpans: []` rather than an error, so an empty array does not mean the run recorded no spans.",
|
||||
"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.",
|
||||
"tags": ["Logs"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "runId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The unique run identifier shared by lifecycle and diagnostic resources.",
|
||||
"description": "Unique workflow run identifier.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The unique run identifier shared by lifecycle and diagnostic resources."
|
||||
"maxLength": 128,
|
||||
"pattern": "^[A-Za-z0-9._:-]+$",
|
||||
"description": "Unique workflow run identifier."
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -363,7 +369,7 @@
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings > API Keys. Operations that reject workspace keys say so in their own description."
|
||||
"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": {
|
||||
@@ -398,13 +404,13 @@
|
||||
}
|
||||
},
|
||||
"Retry-After": {
|
||||
"description": "Seconds to wait before retrying. Sent on `429` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset.",
|
||||
"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` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset."
|
||||
"description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset."
|
||||
}
|
||||
},
|
||||
"X-Run-Id": {
|
||||
@@ -419,7 +425,7 @@
|
||||
},
|
||||
"responses": {
|
||||
"BadRequest": {
|
||||
"description": "The request is invalid.",
|
||||
"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": {
|
||||
@@ -449,7 +455,7 @@
|
||||
}
|
||||
},
|
||||
"Forbidden": {
|
||||
"description": "The caller lacks the rights this operation requires. Where the cause is one a caller can act on, `error.details.code` names it, drawn 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\nA resource in a workspace the caller cannot reach at all answers `404`, not `403`, so absence and denial are indistinguishable to a caller who was never entitled to tell them apart.",
|
||||
"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": {
|
||||
@@ -479,7 +485,7 @@
|
||||
}
|
||||
},
|
||||
"RunIdConflict": {
|
||||
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
|
||||
"description": "The run cannot be started, for one of two causes named by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already claimed, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has reached the maximum workflow-to-workflow call depth.",
|
||||
"headers": {
|
||||
"X-Run-Id": {
|
||||
"$ref": "#/components/headers/X-Run-Id"
|
||||
@@ -493,18 +499,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Gone": {
|
||||
"description": "The requested generated resource has expired.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/V2Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayloadTooLarge": {
|
||||
"description": "The request, or a resource collection it must materialize, exceeds the allowed size. Besides an oversized request body, this covers a generated artifact that renders past the download ceiling and a workspace folder tree too large to load in full.",
|
||||
"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": {
|
||||
@@ -549,7 +545,7 @@
|
||||
}
|
||||
},
|
||||
"ClientClosedRequest": {
|
||||
"description": "The client closed the connection before the response was produced.",
|
||||
"description": "The client closed the connection before the response was produced. An abort can leave the run going, so `error.details.runId` carries the run id — reconcile against the runs resource rather than starting another run.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -569,7 +565,7 @@
|
||||
}
|
||||
},
|
||||
"ServiceUnavailable": {
|
||||
"description": "A required service is temporarily unavailable. The condition is transient, so the response normally carries `Retry-After` with the number of seconds to wait; treat that value as a floor and add jitter before retrying. One case deliberately omits the header: when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, the run may already have started, so retrying could start and bill a second run. Reconcile against the returned run id instead of retrying.",
|
||||
"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"
|
||||
@@ -600,7 +596,7 @@
|
||||
"description": "Human-readable explanation of the error."
|
||||
},
|
||||
"details": {
|
||||
"description": "Optional structured 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address."
|
||||
}
|
||||
},
|
||||
"required": ["code", "message"],
|
||||
@@ -661,7 +657,7 @@
|
||||
"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 run to completion and the run is waiting to be resumed again. **This differs from the run resources for the same run:** `GET /api/v2/workflows/{id}/runs` and `GET /api/v2/workflows/{id}/runs/{runId}` additionally report `paused` for a run held at a human-in-the-loop pause point, which this field reports as `pending`. Use the run resources when the pause state matters."
|
||||
"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",
|
||||
@@ -987,7 +983,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -1057,7 +1053,7 @@
|
||||
"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 run to completion and the run is waiting to be resumed again. **This differs from the run resources for the same run:** `GET /api/v2/workflows/{id}/runs` and `GET /api/v2/workflows/{id}/runs/{runId}` additionally report `paused` for a run held at a human-in-the-loop pause point, which this field reports as `pending`. Use the run resources when the pause state matters."
|
||||
"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",
|
||||
@@ -1144,13 +1140,15 @@
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Canonical slash-prefixed folder path. `/` is the workspace root."
|
||||
"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": "Workflow folder path, or null when unavailable."
|
||||
"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": [
|
||||
@@ -1234,7 +1232,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Workflow graph snapshot captured for the run, with credential values redacted: `oauth-input`, `password: true`, and table sub-block values are null; sensitive nested tool parameters and every parameter without authoritative codec metadata are null; and `{{VAR}}` references in non-opaque fields are preserved. Null when no snapshot is retained."
|
||||
"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",
|
||||
|
||||
+110
-103
@@ -152,9 +152,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -210,7 +210,7 @@
|
||||
"get": {
|
||||
"operationId": "listMcpServers",
|
||||
"summary": "List MCP Servers",
|
||||
"description": "List MCP servers registered in a workspace. Request-header values and OAuth client secrets are never returned. Nothing caps how many servers a workspace registers, so this list is paginated: paginate with `limit` and `cursor`, stopping when `nextCursor` is null. `connectionStatus`, `toolCount`, `lastError`, and `lastToolsRefresh` describe the most recent tool discovery and stay at their registration defaults until one runs — call `GET /api/v2/mcp-servers/{id}/tools` to run it.",
|
||||
"description": "List MCP servers registered in a workspace. Request-header values and OAuth client secrets are never returned. The discovery fields stay at their registration defaults until `GET /api/v2/mcp-servers/{id}/tools` runs a discovery.",
|
||||
"tags": ["MCP Servers"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -240,10 +240,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "createdAt",
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": ["name", "createdAt", "updatedAt"]
|
||||
}
|
||||
@@ -277,9 +277,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -333,7 +333,7 @@
|
||||
"post": {
|
||||
"operationId": "createMcpServer",
|
||||
"summary": "Create MCP Server",
|
||||
"description": "Register an MCP server in a workspace. The endpoint URL determines server identity, must be absolute HTTP or HTTPS, and cannot contain environment-variable references. Header values and OAuth client secrets are write-only. `transport`, `timeout`, `retries`, and `enabled` are applied server-side when omitted; the effective values are in the response.",
|
||||
"description": "Register an MCP server in a workspace. The endpoint URL is the server identity, so a URL already registered here is a `409` — reconfigure that server with `PATCH /api/v2/mcp-servers/{id}` instead. Registration never connects to the endpoint: the server comes back `disconnected` and stays unavailable until `GET /api/v2/mcp-servers/{id}/tools` succeeds.",
|
||||
"tags": ["MCP Servers"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -383,6 +383,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -406,11 +409,11 @@
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "MCP server the operation acts on.",
|
||||
"description": "Unique MCP server identifier.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "MCP server the operation acts on."
|
||||
"description": "Unique MCP server identifier."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -473,18 +476,18 @@
|
||||
"patch": {
|
||||
"operationId": "updateMcpServer",
|
||||
"summary": "Update MCP Server",
|
||||
"description": "Update the supplied MCP server fields. The URL is immutable because it determines server identity; delete and recreate the server to change endpoints. Two fields do not follow the omitted-fields-are-retained rule. `headers` is replaced wholesale rather than merged: sending it drops every stored header it does not repeat, and the only way to keep a header is to resend it. Changing `oauthClientId`, or sending `oauthClientSecret` as null or a new value, revokes the stored OAuth grant and forces reauthorization; switching away from OAuth authentication revokes it too.",
|
||||
"description": "Update the supplied MCP server fields. Omitted fields are retained, except where a field says otherwise. Any change that invalidates authentication revokes the stored OAuth grant, resets `connectionStatus` to `disconnected`, and clears `lastConnected` and `lastError`, so the server must be rediscovered.",
|
||||
"tags": ["MCP Servers"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "MCP server the operation acts on.",
|
||||
"description": "Unique MCP server identifier.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "MCP server the operation acts on."
|
||||
"description": "Unique MCP server identifier."
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -533,6 +536,9 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -554,11 +560,11 @@
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "MCP server the operation acts on.",
|
||||
"description": "Unique MCP server identifier.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "MCP server the operation acts on."
|
||||
"description": "Unique MCP server identifier."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -623,18 +629,18 @@
|
||||
"get": {
|
||||
"operationId": "listMcpServerTools",
|
||||
"summary": "List MCP Server Tools",
|
||||
"description": "Connect to a registered MCP server and return the tools it exposes. Unlike most reads this one has side effects: it opens a live connection to the third-party server and writes `connectionStatus`, `toolCount`, `lastError`, and `lastToolsRefresh` on the server resource, so registering a server and then calling this completes onboarding without opening the Sim UI. Because the pass is not a safe read, a `HEAD` request is answered with an empty `200` without connecting or writing, so it reports only that the endpoint exists and the caller is authorized. Results are served from a short-lived per-workspace cache, so an uncached call reflects whichever workspace member last ran discovery; pass `refresh=true` to reconnect under your own credentials and pick up tools added since the last pass, at the cost of a live round trip to the server. The set is bounded by discovery itself — at most 1,000 tools and 5 MB of tool payload per server. The bounded set is returned in one page with `nextCursor` always null; there is no second page to fetch. An unreachable, slow, or cooling-down server is a `503`; a server whose stored OAuth grant no longer works is a `409` with `error.details.code` `MCP_SERVER_REAUTHORIZATION_REQUIRED`, meaning the registration is intact but a human must reauthorize it in Sim — your API key is fine and re-issuing it changes nothing. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key. Discovery resolves the calling user's own OAuth credentials for the server, which a workspace key cannot supply — so a workspace key that can register a server cannot list its tools.",
|
||||
"description": "Connect to a registered MCP server and return the tools it exposes. This read has side effects: it opens a live connection to the third-party server and writes `connectionStatus`, `toolCount`, `lastError`, and `lastToolsRefresh`. A `HEAD` skips the effect but is authorized exactly as the `GET` is, so it answers `400`, `401`, `403`, or `404` wherever the `GET` would and an empty `200` otherwise. Skipping the effect means skipping the read that produces the payload, so that `200` carries none of the response headers documented below — it answers whether the `GET` would be allowed, not what the `GET` would return. Discovery is bounded at 1,000 tools and 5 MB of tool payload per server. The bounded set is returned in one page; `nextCursor` is always null. An unreachable, slow, or cooling-down server is a `503`; a stored OAuth grant that no longer works is a `409` with `error.details.code` `MCP_SERVER_REAUTHORIZATION_REQUIRED`, which only a human reauthorizing in Sim can clear. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["MCP Servers"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "MCP server the operation acts on.",
|
||||
"description": "Unique MCP server identifier.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "MCP server the operation acts on."
|
||||
"description": "Unique MCP server identifier."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -652,9 +658,9 @@
|
||||
"name": "refresh",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Bypass the cached tool list and reconnect to the server. Slower, and the only way to pick up a tool added since the last refresh.",
|
||||
"description": "Bypass the short-lived per-workspace tool cache and reconnect under your own credentials. A cached result reflects whichever workspace member last ran discovery, so this is the only way to pick up a tool added since then; it costs a live round trip.",
|
||||
"schema": {
|
||||
"description": "Bypass the cached tool list and reconnect to the server. Slower, and the only way to pick up a tool added since the last refresh.",
|
||||
"description": "Bypass the short-lived per-workspace tool cache and reconnect under your own credentials. A cached result reflects whichever workspace member last ran discovery, so this is the only way to pick up a tool added since then; it costs a live round trip.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
@@ -712,7 +718,7 @@
|
||||
"get": {
|
||||
"operationId": "listSkills",
|
||||
"summary": "List Skills",
|
||||
"description": "List workspace and built-in skills. Built-ins are marked read-only. The list omits skill bodies; fetch one skill to read its content. Paginate with `limit` and `cursor`, stopping when `nextCursor` is null.",
|
||||
"description": "List workspace and built-in skills with opaque cursor pagination. Built-ins are marked read-only. The list omits skill bodies; fetch one skill to read its content.",
|
||||
"tags": ["Skills"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -742,10 +748,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "createdAt",
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": ["name", "createdAt", "updatedAt"]
|
||||
}
|
||||
@@ -779,9 +785,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -835,7 +841,7 @@
|
||||
"post": {
|
||||
"operationId": "createSkill",
|
||||
"summary": "Create Skill",
|
||||
"description": "Create one skill in a workspace. Its kebab-case name must be unique and cannot be reserved by a built-in skill. Note that a workspace API key may create a skill but may not later update or delete it.",
|
||||
"description": "Create one skill in a workspace. Its kebab-case name must be unique and cannot be reserved by a built-in skill. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Skills"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -885,6 +891,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -908,11 +917,11 @@
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Skill to retrieve, update, or delete. Built-in skills use their name as the id.",
|
||||
"description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Skill to retrieve, update, or delete. Built-in skills use their name as the id."
|
||||
"description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -975,18 +984,18 @@
|
||||
"patch": {
|
||||
"operationId": "updateSkill",
|
||||
"summary": "Update Skill",
|
||||
"description": "Update the supplied fields on a workspace skill. Omitted fields retain their stored values. Built-in skills are read-only. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key.",
|
||||
"description": "Update the supplied fields on a workspace skill. Omitted fields retain their stored values. Built-in skills are read-only. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Skills"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Skill to retrieve, update, or delete. Built-in skills use their name as the id.",
|
||||
"description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Skill to retrieve, update, or delete. Built-in skills use their name as the id."
|
||||
"description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`."
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1038,6 +1047,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1052,18 +1064,18 @@
|
||||
"delete": {
|
||||
"operationId": "deleteSkill",
|
||||
"summary": "Delete Skill",
|
||||
"description": "Delete a workspace skill. Built-in skills are read-only and cannot be deleted. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key.",
|
||||
"description": "Delete a workspace skill. Built-in skills are read-only and cannot be deleted. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Skills"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Skill to retrieve, update, or delete. Built-in skills use their name as the id.",
|
||||
"description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Skill to retrieve, update, or delete. Built-in skills use their name as the id."
|
||||
"description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1128,7 +1140,7 @@
|
||||
"get": {
|
||||
"operationId": "listCustomTools",
|
||||
"summary": "List Custom Tools",
|
||||
"description": "List code-backed custom tools defined in a workspace. Legacy personal tools are excluded. Paginate with `limit` and `cursor`, stopping when `nextCursor` is null.",
|
||||
"description": "List code-backed custom tools defined in a workspace, with opaque cursor pagination. Legacy personal tools are excluded.",
|
||||
"tags": ["Custom Tools"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1195,9 +1207,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -1301,6 +1313,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1324,11 +1339,11 @@
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Custom tool to retrieve, update, or delete.",
|
||||
"description": "Unique custom tool identifier.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Custom tool to retrieve, update, or delete."
|
||||
"description": "Unique custom tool identifier."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1398,11 +1413,11 @@
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Custom tool to retrieve, update, or delete.",
|
||||
"description": "Unique custom tool identifier.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Custom tool to retrieve, update, or delete."
|
||||
"description": "Unique custom tool identifier."
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1454,6 +1469,9 @@
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1475,11 +1493,11 @@
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Custom tool to retrieve, update, or delete.",
|
||||
"description": "Unique custom tool identifier.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Custom tool to retrieve, update, or delete."
|
||||
"description": "Unique custom tool identifier."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1544,7 +1562,7 @@
|
||||
"get": {
|
||||
"operationId": "listCredentials",
|
||||
"summary": "List Credentials",
|
||||
"description": "List OAuth and service-account connections visible to the caller. Secret material is never returned. Credential mutations and single-resource reads are intentionally not exposed. Paginate with `limit` and `cursor`, stopping when `nextCursor` is null.",
|
||||
"description": "List OAuth and service-account connections visible to the caller. Secret material is never returned. Credential mutations and single-resource reads are not exposed.",
|
||||
"tags": ["Credentials"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1633,9 +1651,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -1691,7 +1709,7 @@
|
||||
"get": {
|
||||
"operationId": "listSecrets",
|
||||
"summary": "List Secrets",
|
||||
"description": "List workspace and caller-owned personal secret metadata. Only names, scope, role, and timestamps are returned; secret values are never read or returned. Paginate with `limit` and `cursor`, stopping when `nextCursor` is null. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key.",
|
||||
"description": "List workspace and caller-owned personal secret metadata with opaque cursor pagination. Only names, scope, role, and timestamps are returned; secret values are never returned. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Secrets"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1732,10 +1750,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "name",
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": ["name", "createdAt", "updatedAt"]
|
||||
}
|
||||
@@ -1769,9 +1787,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -1827,7 +1845,7 @@
|
||||
"put": {
|
||||
"operationId": "setSecret",
|
||||
"summary": "Set Secret",
|
||||
"description": "Create or replace a workspace or caller-owned personal secret. The value is encrypted at rest, is write-only, and is never included in the response. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key.",
|
||||
"description": "Create or replace a workspace or caller-owned personal secret. The value is encrypted at rest, is write-only, and is never included in the response. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Secrets"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1910,6 +1928,9 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
},
|
||||
@@ -1924,7 +1945,7 @@
|
||||
"delete": {
|
||||
"operationId": "deleteSecret",
|
||||
"summary": "Delete Secret",
|
||||
"description": "Delete a workspace or caller-owned personal secret without reading or returning its stored value. A workspace API key cannot call this operation and is rejected with `403`; use a personal API key.",
|
||||
"description": "Delete a workspace or caller-owned personal secret without reading or returning its stored value. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Secrets"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -2016,7 +2037,7 @@
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings > API Keys. Operations that reject workspace keys say so in their own description."
|
||||
"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": {
|
||||
@@ -2051,13 +2072,13 @@
|
||||
}
|
||||
},
|
||||
"Retry-After": {
|
||||
"description": "Seconds to wait before retrying. Sent on `429` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset.",
|
||||
"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` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset."
|
||||
"description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset."
|
||||
}
|
||||
},
|
||||
"X-Run-Id": {
|
||||
@@ -2072,7 +2093,7 @@
|
||||
},
|
||||
"responses": {
|
||||
"BadRequest": {
|
||||
"description": "The request is invalid.",
|
||||
"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": {
|
||||
@@ -2102,7 +2123,7 @@
|
||||
}
|
||||
},
|
||||
"Forbidden": {
|
||||
"description": "The caller lacks the rights this operation requires. Where the cause is one a caller can act on, `error.details.code` names it, drawn 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\nA resource in a workspace the caller cannot reach at all answers `404`, not `403`, so absence and denial are indistinguishable to a caller who was never entitled to tell them apart.",
|
||||
"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": {
|
||||
@@ -2132,7 +2153,7 @@
|
||||
}
|
||||
},
|
||||
"RunIdConflict": {
|
||||
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
|
||||
"description": "The run cannot be started, for one of two causes named by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already claimed, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has reached the maximum workflow-to-workflow call depth.",
|
||||
"headers": {
|
||||
"X-Run-Id": {
|
||||
"$ref": "#/components/headers/X-Run-Id"
|
||||
@@ -2146,18 +2167,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Gone": {
|
||||
"description": "The requested generated resource has expired.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/V2Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayloadTooLarge": {
|
||||
"description": "The request, or a resource collection it must materialize, exceeds the allowed size. Besides an oversized request body, this covers a generated artifact that renders past the download ceiling and a workspace folder tree too large to load in full.",
|
||||
"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": {
|
||||
@@ -2202,7 +2213,7 @@
|
||||
}
|
||||
},
|
||||
"ClientClosedRequest": {
|
||||
"description": "The client closed the connection before the response was produced.",
|
||||
"description": "The client closed the connection before the response was produced. An abort can leave the run going, so `error.details.runId` carries the run id — reconcile against the runs resource rather than starting another run.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -2222,7 +2233,7 @@
|
||||
}
|
||||
},
|
||||
"ServiceUnavailable": {
|
||||
"description": "A required service is temporarily unavailable. The condition is transient, so the response normally carries `Retry-After` with the number of seconds to wait; treat that value as a floor and add jitter before retrying. One case deliberately omits the header: when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, the run may already have started, so retrying could start and bill a second run. Reconcile against the returned run id instead of retrying.",
|
||||
"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"
|
||||
@@ -2253,7 +2264,7 @@
|
||||
"description": "Human-readable explanation of the error."
|
||||
},
|
||||
"details": {
|
||||
"description": "Optional structured 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address."
|
||||
}
|
||||
},
|
||||
"required": ["code", "message"],
|
||||
@@ -2382,7 +2393,7 @@
|
||||
},
|
||||
"isExternal": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the member belongs to a different organization than the workspace. True for an explicitly granted member whose own organization differs from the workspace's; false for the workspace owner and for a member sharing the workspace organization. Inherited organization-administrator access is always reported as false, so this is not a signal that access came from outside the explicit member list."
|
||||
"description": "Whether the member belongs to a different organization than the workspace. True only for an explicitly granted member whose own organization differs; inherited organization-administrator access is always reported as false, so this does not detect every outside caller."
|
||||
},
|
||||
"joinedAt": {
|
||||
"type": "string",
|
||||
@@ -2415,7 +2426,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -2481,12 +2492,12 @@
|
||||
"description": "Whether the server tools are available to workflows."
|
||||
},
|
||||
"connectionStatus": {
|
||||
"description": "Result of the most recent connection attempt.",
|
||||
"description": "Result of the most recent connection attempt. Registration and re-registration store a configuration without contacting the endpoint, so a server begins — and returns to — `disconnected` until a tool discovery runs.",
|
||||
"type": "string",
|
||||
"enum": ["connected", "disconnected", "error"]
|
||||
},
|
||||
"lastError": {
|
||||
"description": "Message from the most recent failed connection, or null when absent.",
|
||||
"description": "Message from the most recent failed connection, or null when absent. A re-registration clears it, since the configuration it described no longer applies.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
@@ -2507,7 +2518,7 @@
|
||||
"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))$"
|
||||
},
|
||||
"lastConnected": {
|
||||
"description": "ISO 8601 timestamp of the most recent successful connection.",
|
||||
"description": "ISO 8601 timestamp of the most recent successful connection. Absent until the server completes one; registering a server does not set it.",
|
||||
"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))$"
|
||||
@@ -2579,7 +2590,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -2639,11 +2650,9 @@
|
||||
"timeout": 30000,
|
||||
"retries": 3,
|
||||
"enabled": true,
|
||||
"connectionStatus": "connected",
|
||||
"connectionStatus": "disconnected",
|
||||
"lastError": null,
|
||||
"toolCount": 7,
|
||||
"lastToolsRefresh": "2026-06-20T14:02:11.000Z",
|
||||
"lastConnected": "2026-06-20T14:02:11.000Z",
|
||||
"toolCount": 0,
|
||||
"createdAt": "2026-06-01T09:14:00.000Z",
|
||||
"updatedAt": "2026-06-20T14:02:11.000Z",
|
||||
"hasHeaders": true,
|
||||
@@ -2682,15 +2691,16 @@
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 2048,
|
||||
"description": "Absolute HTTP or HTTPS endpoint URL without `{{ENV_VAR}}` references."
|
||||
"description": "Absolute HTTP or HTTPS endpoint URL without `{{ENV_VAR}}` references. It determines server identity and is immutable: delete and recreate the server to change endpoints."
|
||||
},
|
||||
"authType": {
|
||||
"description": "Authentication method. Sim detects it from the server when omitted.",
|
||||
"description": "Authentication method. Applied server-side as `headers` when omitted; registration never contacts the server, so an omitted value is never detected from it.",
|
||||
"default": "headers",
|
||||
"type": "string",
|
||||
"enum": ["none", "headers", "oauth"]
|
||||
},
|
||||
"headers": {
|
||||
"description": "Write-only request headers sent to the server.",
|
||||
"description": "Write-only request headers sent to the server. Replaced wholesale rather than merged on update: sending this field drops every stored header it does not repeat.",
|
||||
"writeOnly": true,
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
@@ -2722,7 +2732,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"oauthClientId": {
|
||||
"description": "Pre-registered OAuth client identifier.",
|
||||
"description": "Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -2734,7 +2744,7 @@
|
||||
]
|
||||
},
|
||||
"oauthClientSecret": {
|
||||
"description": "Write-only pre-registered OAuth client secret.",
|
||||
"description": "Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication.",
|
||||
"writeOnly": true,
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2871,12 +2881,13 @@
|
||||
"maxLength": 2048
|
||||
},
|
||||
"authType": {
|
||||
"description": "Authentication method. Sim detects it from the server when omitted.",
|
||||
"description": "Authentication method. Applied server-side as `headers` when omitted; registration never contacts the server, so an omitted value is never detected from it.",
|
||||
"default": "headers",
|
||||
"type": "string",
|
||||
"enum": ["none", "headers", "oauth"]
|
||||
},
|
||||
"headers": {
|
||||
"description": "Write-only request headers sent to the server.",
|
||||
"description": "Write-only request headers sent to the server. Replaced wholesale rather than merged on update: sending this field drops every stored header it does not repeat.",
|
||||
"writeOnly": true,
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
@@ -2908,7 +2919,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"oauthClientId": {
|
||||
"description": "Pre-registered OAuth client identifier.",
|
||||
"description": "Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -2920,7 +2931,7 @@
|
||||
]
|
||||
},
|
||||
"oauthClientSecret": {
|
||||
"description": "Write-only pre-registered OAuth client secret.",
|
||||
"description": "Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication.",
|
||||
"writeOnly": true,
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -3019,10 +3030,6 @@
|
||||
"type": "string",
|
||||
"description": "Name of a required argument."
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"description": "Description of the argument object.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["type"],
|
||||
@@ -3064,7 +3071,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
|
||||
}
|
||||
},
|
||||
"required": ["data", "nextCursor"],
|
||||
@@ -3100,7 +3107,7 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique skill identifier. Built-in skills use their name as the id."
|
||||
"description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
@@ -3151,7 +3158,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -3179,7 +3186,7 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique skill identifier. Built-in skills use their name as the id."
|
||||
"description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
@@ -3529,7 +3536,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -4041,7 +4048,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -4125,7 +4132,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
|
||||
+536
-148
File diff suppressed because it is too large
Load Diff
+135
-101
@@ -40,7 +40,7 @@
|
||||
"get": {
|
||||
"operationId": "listWorkflows",
|
||||
"summary": "List Workflows",
|
||||
"description": "List workflows in a workspace with folder and deployment filters, search, sorting, and opaque cursor pagination. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "List workflows in a workspace with folder and deployment filters, search, sorting, and opaque cursor pagination. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Workflows"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -58,10 +58,10 @@
|
||||
"name": "folderPath",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Restrict results to workflows in this folder path.",
|
||||
"description": "Restrict results to workflows in this folder path. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
|
||||
"schema": {
|
||||
"description": "Restrict results to workflows in this folder path.",
|
||||
"type": "string"
|
||||
"description": "Restrict results to workflows in this folder path. A path that names no folder narrows the result to nothing, so the response is an empty page rather than an error.",
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -91,9 +91,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -102,9 +102,9 @@
|
||||
"name": "search",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Case-insensitive substring search on the resource name.",
|
||||
"description": "Case-insensitive substring match against the resource name.",
|
||||
"schema": {
|
||||
"description": "Case-insensitive substring search on the resource name.",
|
||||
"description": "Case-insensitive substring match against the resource name.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 200
|
||||
@@ -114,10 +114,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "position",
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": ["position", "name", "createdAt", "updatedAt", "runCount"]
|
||||
}
|
||||
@@ -186,7 +186,7 @@
|
||||
"post": {
|
||||
"operationId": "createWorkflowV2",
|
||||
"summary": "Create Workflow",
|
||||
"description": "Create a workflow in a workspace root or canonical workflow folder. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "Create a workflow in a workspace root or canonical workflow folder. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Workflows"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -258,7 +258,7 @@
|
||||
"get": {
|
||||
"operationId": "getWorkflow",
|
||||
"summary": "Get Workflow",
|
||||
"description": "Get a workflow with its variables and deployed API-trigger inputs. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "Get a workflow with its variables and deployed API-trigger inputs. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Workflows"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -325,7 +325,7 @@
|
||||
"patch": {
|
||||
"operationId": "updateWorkflowV2",
|
||||
"summary": "Update Workflow",
|
||||
"description": "Rename, describe, or move a workflow to a canonical folder path. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "Rename, describe, or move a workflow to a canonical folder path. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Workflows"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -510,9 +510,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -590,7 +590,7 @@
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"maximum": 2147483647,
|
||||
"description": "Numeric deployment version.",
|
||||
"examples": [3]
|
||||
}
|
||||
@@ -646,7 +646,7 @@
|
||||
"get": {
|
||||
"operationId": "getWorkflowDeployment",
|
||||
"summary": "Get Workflow Deployment",
|
||||
"description": "Read the current deployment state of a workflow: whether a version is live, when it went live, the most recent deployment attempt with its readiness and failure payload, and whether the editable draft has since diverged from the live version. This is the only place `needsRedeployment` is published — the deploy, undeploy, and rollback responses cannot carry it, because they answer at the moment the draft and the live version are equal.",
|
||||
"description": "Read the current deployment state of a workflow: whether a version is live, when it went live, the most recent deployment attempt with its readiness and failure payload, and whether the editable draft has since diverged from the live version. This is the only operation that publishes `needsRedeployment`.",
|
||||
"tags": ["Workflows"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -712,7 +712,7 @@
|
||||
"post": {
|
||||
"operationId": "deployWorkflow",
|
||||
"summary": "Deploy Workflow",
|
||||
"description": "Create and asynchronously activate a deployment version. This request is not idempotent: it accepts no idempotency key and every call mints a new deployment version, so retrying after a timeout creates a second version rather than returning the first. The response carries `latestDeploymentAttempt` for the accepted attempt, but `GET /workflows/{id}` does not expose that field — poll activation with `isDeployed` and `deployedAt` on the workflow, or with `isActive` on `GET /workflows/{id}/versions`. Returns 409 when the deployment would conflict with an existing webhook path. A workspace API key cannot call this operation. Because unauthorized resources are concealed, the rejection is reported as `404` rather than `403`; use a personal API key.",
|
||||
"description": "Create and asynchronously activate a deployment version. Not idempotent: every call mints a new version, so a retry after a timeout creates a second one. A deployment that would conflict with an existing webhook path is a `409`. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Workflows"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -796,7 +796,7 @@
|
||||
"delete": {
|
||||
"operationId": "undeployWorkflow",
|
||||
"summary": "Undeploy Workflow",
|
||||
"description": "Deactivate the currently serving workflow version. A workspace API key cannot call this operation. Because unauthorized resources are concealed, the rejection is reported as `404` rather than `403`; use a personal API key.",
|
||||
"description": "Deactivate the currently serving workflow version. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Workflows"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -865,7 +865,7 @@
|
||||
"post": {
|
||||
"operationId": "rollbackWorkflow",
|
||||
"summary": "Rollback Workflow",
|
||||
"description": "Asynchronously reactivate a previous deployment version, selecting the preceding active version when no version is supplied. A workspace API key cannot call this operation. Because unauthorized resources are concealed, the rejection is reported as `404` rather than `403`; use a personal API key.",
|
||||
"description": "Asynchronously reactivate a previous deployment version, selecting the preceding active version when no version is supplied. A workspace API key is rejected with `403`; use a personal API key.",
|
||||
"tags": ["Workflows"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -926,6 +926,9 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
@@ -948,7 +951,7 @@
|
||||
"get": {
|
||||
"operationId": "exportWorkflow",
|
||||
"summary": "Export Workflow",
|
||||
"description": "Export a portable, secret-sanitized workflow. Workspace-scoped bindings must be selected again after import. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
|
||||
"description": "Export a portable, secret-sanitized workflow. Workspace-scoped bindings must be selected again after import. Exporting records an audit event, so it is not a safe read. A `HEAD` skips the effect but is authorized exactly as the `GET` is, so it answers `400`, `401`, `403`, or `404` wherever the `GET` would and an empty `200` otherwise. Skipping the effect means skipping the read that produces the payload, so that `200` carries none of the response headers documented below — it answers whether the `GET` would be allowed, not what the `GET` would return. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Workflows"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1017,7 +1020,7 @@
|
||||
"post": {
|
||||
"operationId": "importWorkflow",
|
||||
"summary": "Import Workflow",
|
||||
"description": "Create a workflow from a portable export object, bare state, or JSON string.",
|
||||
"description": "Create a workflow from a portable export object, bare state, or JSON string. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Workflows"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -1089,7 +1092,7 @@
|
||||
"post": {
|
||||
"operationId": "executeWorkflowV2",
|
||||
"summary": "Execute Workflow",
|
||||
"description": "Execute a deployed workflow synchronously, asynchronously, or as Server-Sent Events. Public workflows permit anonymous synchronous and streaming execution; asynchronous execution requires an API key. A synchronous run that exceeds its execution timeout returns HTTP 200 with `status: \"failed\"` and `error.code: \"TIMEOUT\"` rather than an HTTP error, so branch on `status`. The optional `X-Run-Id` header is a one-shot uniqueness claim, not an idempotency key: reusing a value returns 409 with `error.details.code: \"RUN_ID_CONFLICT\"` and never replays the earlier run. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require `stream: true`. (6) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
|
||||
"description": "Execute a deployed workflow synchronously, asynchronously, or as Server-Sent Events. Public workflows permit anonymous synchronous and streaming execution; asynchronous execution requires an API key. A synchronous run that exceeds its execution timeout returns HTTP 200 with `status: \"failed\"` and `error.code: \"TIMEOUT\"` rather than an HTTP error, so branch on `status`. Each option carries the modes it requires and the modes that reject it; a violated combination is a 400.",
|
||||
"tags": ["Workflows"],
|
||||
"security": [
|
||||
{
|
||||
@@ -1114,9 +1117,9 @@
|
||||
"name": "x-run-id",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"description": "Caller-supplied run identifier, available only to API-key callers. This is a one-shot uniqueness claim, NOT an idempotency key: the first request to use a value starts a run, and any later request reusing it fails with 409 and `error.details.code: \"RUN_ID_CONFLICT\"` instead of replaying the original result. To retry safely, generate a fresh value per attempt and reconcile duplicates yourself, or omit the header and let the server allocate the run identifier.",
|
||||
"description": "Caller-supplied run identifier, available only to API-key callers. A one-shot uniqueness claim, NOT an idempotency key: reusing a value fails with `409` and `error.details.code: \"RUN_ID_CONFLICT\"` rather than replaying the original result. To retry safely, send a fresh value per attempt, or omit the header and let the server allocate one.",
|
||||
"schema": {
|
||||
"description": "Caller-supplied run identifier, available only to API-key callers. This is a one-shot uniqueness claim, NOT an idempotency key: the first request to use a value starts a run, and any later request reusing it fails with 409 and `error.details.code: \"RUN_ID_CONFLICT\"` instead of replaying the original result. To retry safely, generate a fresh value per attempt and reconcile duplicates yourself, or omit the header and let the server allocate the run identifier.",
|
||||
"description": "Caller-supplied run identifier, available only to API-key callers. A one-shot uniqueness claim, NOT an idempotency key: reusing a value fails with `409` and `error.details.code: \"RUN_ID_CONFLICT\"` rather than replaying the original result. To retry safely, send a fresh value per attempt, or omit the header and let the server allocate one.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 128,
|
||||
@@ -1128,16 +1131,16 @@
|
||||
"name": "x-sim-via",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"description": "Comma-separated workflow identifiers describing the workflow-to-workflow call chain that led to this request. Each hop appends its own workflow id, and Sim sets it automatically when one workflow calls another; supply it yourself only when relaying an existing chain. A chain already at the maximum depth is rejected with 409 and `error.details.code: \"CALL_CHAIN_DEPTH_EXCEEDED\"`, which is how runaway recursion between workflows is stopped.",
|
||||
"description": "Comma-separated workflow identifiers naming the workflow-to-workflow call chain that led to this request. Each hop appends its own workflow id, and Sim sets it automatically; supply it yourself only when relaying an existing chain. A chain at the maximum depth is rejected with `409` and `error.details.code: \"CALL_CHAIN_DEPTH_EXCEEDED\"`.",
|
||||
"schema": {
|
||||
"description": "Comma-separated workflow identifiers describing the workflow-to-workflow call chain that led to this request. Each hop appends its own workflow id, and Sim sets it automatically when one workflow calls another; supply it yourself only when relaying an existing chain. A chain already at the maximum depth is rejected with 409 and `error.details.code: \"CALL_CHAIN_DEPTH_EXCEEDED\"`, which is how runaway recursion between workflows is stopped.",
|
||||
"description": "Comma-separated workflow identifiers naming the workflow-to-workflow call chain that led to this request. Each hop appends its own workflow id, and Sim sets it automatically; supply it yourself only when relaying an existing chain. A chain at the maximum depth is rejected with `409` and `error.details.code: \"CALL_CHAIN_DEPTH_EXCEEDED\"`.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"description": "Input and execution-mode options for a deployed workflow. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require `stream: true`. (6) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
|
||||
"description": "Input and execution-mode options for a deployed workflow. Each option carries the modes it requires and the modes that reject it; a violated combination is a 400.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -1240,7 +1243,7 @@
|
||||
"get": {
|
||||
"operationId": "listWorkflowRunsV2",
|
||||
"summary": "List Workflow Runs",
|
||||
"description": "List recorded runs of a workflow with filtering and opaque cursor pagination. Ordering deviates from the v2 `sortBy` + `sortOrder` convention: runs are sortable only by start time, so direction is carried by the single `order` param.",
|
||||
"description": "List recorded runs of a workflow with filtering and opaque cursor pagination. 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": ["Workflow Runs"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1281,24 +1284,24 @@
|
||||
"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.",
|
||||
"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."
|
||||
"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.",
|
||||
"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."
|
||||
"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."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1318,9 +1321,9 @@
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque cursor returned by the previous page.",
|
||||
"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 returned by the previous page.",
|
||||
"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
|
||||
}
|
||||
@@ -1329,10 +1332,10 @@
|
||||
"name": "order",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Sort direction by run start time. This operation deviates from the v2 `sortBy` + `sortOrder` convention: runs are sortable only by start time, so the direction is carried by this single `order` param and `sortBy`/`sortOrder` are not accepted.",
|
||||
"description": "Sort direction by run start time. This list is sortable only by run start time, so it takes `order` in place of `sortBy`/`sortOrder`, which it rejects.",
|
||||
"schema": {
|
||||
"default": "desc",
|
||||
"description": "Sort direction by run start time. This operation deviates from the v2 `sortBy` + `sortOrder` convention: runs are sortable only by start time, so the direction is carried by this single `order` param and `sortBy`/`sortOrder` are not accepted.",
|
||||
"description": "Sort direction by run start time. This list is sortable only by run start time, so it takes `order` in place of `sortBy`/`sortOrder`, which it rejects.",
|
||||
"type": "string",
|
||||
"enum": ["asc", "desc"]
|
||||
}
|
||||
@@ -1616,7 +1619,7 @@
|
||||
"post": {
|
||||
"operationId": "cancelRunV2",
|
||||
"summary": "Cancel Workflow Run",
|
||||
"description": "Request cancellation of a running, queued, or paused workflow run. Cancelling a run that has already reached a terminal state succeeds with no effect rather than returning an error. The `reason` field is present on every response, including full successes — `recorded` is the success value; it is not a partial-failure marker. A run produced by a table workflow group is a 409 when its cell can no longer accept the cancellation, because the run and its cell must reach the cancelled state together.",
|
||||
"description": "Request cancellation of a running, queued, or paused workflow run. Cancelling a run already in a terminal state succeeds with no effect. A run produced by a table workflow group is a `409` when its cell can no longer accept the cancellation.",
|
||||
"tags": ["Workflow Runs"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1698,7 +1701,7 @@
|
||||
"get": {
|
||||
"operationId": "listWorkflowsFolders",
|
||||
"summary": "List Workflow Folders",
|
||||
"description": "List canonical workflow folders in a workspace. The bounded set is returned in one page with `nextCursor` always null; there is no second page to fetch.",
|
||||
"description": "List canonical workflow folders in a workspace. The bounded set is returned in one page; `nextCursor` is always null. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Workflows"],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1719,7 +1722,7 @@
|
||||
"description": "Restrict results to direct children of this parent path.",
|
||||
"schema": {
|
||||
"description": "Restrict results to direct children of this parent path.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1738,10 +1741,10 @@
|
||||
"name": "sortBy",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"schema": {
|
||||
"default": "name",
|
||||
"description": "Field used to sort the result.",
|
||||
"description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
|
||||
"type": "string",
|
||||
"enum": ["name", "createdAt", "updatedAt"]
|
||||
}
|
||||
@@ -1810,7 +1813,7 @@
|
||||
"post": {
|
||||
"operationId": "createWorkflowsFolder",
|
||||
"summary": "Create Workflow Folder",
|
||||
"description": "Create a canonical workflow folder in a workspace.",
|
||||
"description": "Create a canonical workflow folder in a workspace. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Workflows"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -1880,7 +1883,7 @@
|
||||
"patch": {
|
||||
"operationId": "relocateWorkflowsFolder",
|
||||
"summary": "Rename or Move Workflow Folder",
|
||||
"description": "Rename or move a workflow folder and its descendants to a canonical path.",
|
||||
"description": "Rename or move a workflow folder and its descendants to a canonical path. A workspace folder tree over 10,000 folders is a `413`.",
|
||||
"tags": ["Workflows"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -1971,16 +1974,30 @@
|
||||
"description": "Path of the folder to delete.",
|
||||
"schema": {
|
||||
"description": "Path of the folder to delete.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "recursive",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Delete nested files and folders when true.",
|
||||
"description": "Delete the folder's nested files and folders too. An empty folder deletes either way; a non-empty one needs this. The listed spellings are the whole accepted vocabulary and are case-sensitive; any other value is rejected.",
|
||||
"schema": {
|
||||
"description": "Delete nested files and folders when true.",
|
||||
"description": "Delete the folder's nested files and folders too. An empty folder deletes either way; a non-empty one needs this. The listed spellings are the whole accepted vocabulary and are case-sensitive; any other value is rejected.",
|
||||
"enum": [
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
"on",
|
||||
"y",
|
||||
"enabled",
|
||||
"false",
|
||||
"0",
|
||||
"no",
|
||||
"off",
|
||||
"n",
|
||||
"disabled"
|
||||
],
|
||||
"default": "false",
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2048,7 +2065,7 @@
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings > API Keys. Operations that reject workspace keys say so in their own description."
|
||||
"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": {
|
||||
@@ -2083,13 +2100,13 @@
|
||||
}
|
||||
},
|
||||
"Retry-After": {
|
||||
"description": "Seconds to wait before retrying. Sent on `429` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset.",
|
||||
"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` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset."
|
||||
"description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset."
|
||||
}
|
||||
},
|
||||
"X-Run-Id": {
|
||||
@@ -2104,7 +2121,7 @@
|
||||
},
|
||||
"responses": {
|
||||
"BadRequest": {
|
||||
"description": "The request is invalid.",
|
||||
"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": {
|
||||
@@ -2134,7 +2151,7 @@
|
||||
}
|
||||
},
|
||||
"Forbidden": {
|
||||
"description": "The caller lacks the rights this operation requires. Where the cause is one a caller can act on, `error.details.code` names it, drawn 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\nA resource in a workspace the caller cannot reach at all answers `404`, not `403`, so absence and denial are indistinguishable to a caller who was never entitled to tell them apart.",
|
||||
"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": {
|
||||
@@ -2164,7 +2181,7 @@
|
||||
}
|
||||
},
|
||||
"RunIdConflict": {
|
||||
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
|
||||
"description": "The run cannot be started, for one of two causes named by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already claimed, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has reached the maximum workflow-to-workflow call depth.",
|
||||
"headers": {
|
||||
"X-Run-Id": {
|
||||
"$ref": "#/components/headers/X-Run-Id"
|
||||
@@ -2178,18 +2195,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Gone": {
|
||||
"description": "The requested generated resource has expired.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/V2Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayloadTooLarge": {
|
||||
"description": "The request, or a resource collection it must materialize, exceeds the allowed size. Besides an oversized request body, this covers a generated artifact that renders past the download ceiling and a workspace folder tree too large to load in full.",
|
||||
"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": {
|
||||
@@ -2234,7 +2241,7 @@
|
||||
}
|
||||
},
|
||||
"ClientClosedRequest": {
|
||||
"description": "The client closed the connection before the response was produced.",
|
||||
"description": "The client closed the connection before the response was produced. An abort can leave the run going, so `error.details.runId` carries the run id — reconcile against the runs resource rather than starting another run.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -2254,7 +2261,7 @@
|
||||
}
|
||||
},
|
||||
"ServiceUnavailable": {
|
||||
"description": "A required service is temporarily unavailable. The condition is transient, so the response normally carries `Retry-After` with the number of seconds to wait; treat that value as a floor and add jitter before retrying. One case deliberately omits the header: when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, the run may already have started, so retrying could start and bill a second run. Reconcile against the returned run id instead of retrying.",
|
||||
"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"
|
||||
@@ -2285,7 +2292,7 @@
|
||||
"description": "Human-readable explanation of the error."
|
||||
},
|
||||
"details": {
|
||||
"description": "Optional structured 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- `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- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address."
|
||||
}
|
||||
},
|
||||
"required": ["code", "message"],
|
||||
@@ -2306,6 +2313,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"FolderPathInput": {
|
||||
"title": "Folder path input",
|
||||
"description": "Folder path. A missing leading slash is normalized before validation. 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": "string"
|
||||
},
|
||||
"WorkflowListItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2332,7 +2345,9 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"type": "string",
|
||||
"title": "Folder path",
|
||||
"description": "Canonical containing-folder path; `/` is the workspace root.",
|
||||
"maxLength": 4096,
|
||||
"examples": ["/Operations"]
|
||||
},
|
||||
"workspaceId": {
|
||||
@@ -2359,7 +2374,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"description": "Total recorded workflow runs."
|
||||
"description": "Runs that finished successfully. Failed, cancelled, and paused runs are not counted, and the counter is never reduced when a run ages out of log retention — so it does not match the size of `GET /api/v2/workflows/{id}/runs`, in either direction."
|
||||
},
|
||||
"lastRunAt": {
|
||||
"anyOf": [
|
||||
@@ -2370,7 +2385,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "ISO 8601 timestamp of the latest run, or null when never run.",
|
||||
"description": "ISO 8601 timestamp of the latest run counted by `runCount`, or null when none has been. Stamped by the same successful-run path, so a workflow whose only runs failed reports null here.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"createdAt": {
|
||||
@@ -2420,7 +2435,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -2505,8 +2520,7 @@
|
||||
]
|
||||
},
|
||||
"folderPath": {
|
||||
"description": "Folder path. A missing leading slash is normalized before validation.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "name"],
|
||||
@@ -2561,7 +2575,9 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"type": "string",
|
||||
"title": "Folder path",
|
||||
"description": "Canonical containing-folder path; `/` is the workspace root.",
|
||||
"maxLength": 4096,
|
||||
"examples": ["/Operations"]
|
||||
},
|
||||
"workspaceId": {
|
||||
@@ -2588,7 +2604,7 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"description": "Total recorded workflow runs."
|
||||
"description": "Runs that finished successfully. Failed, cancelled, and paused runs are not counted, and the counter is never reduced when a run ages out of log retention — so it does not match the size of `GET /api/v2/workflows/{id}/runs`, in either direction."
|
||||
},
|
||||
"lastRunAt": {
|
||||
"anyOf": [
|
||||
@@ -2599,7 +2615,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "ISO 8601 timestamp of the latest run, or null when never run.",
|
||||
"description": "ISO 8601 timestamp of the latest run counted by `runCount`, or null when none has been. Stamped by the same successful-run path, so a workflow whose only runs failed reports null here.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"createdAt": {
|
||||
@@ -2734,7 +2750,7 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"description": "Destination folder path; `/` moves the workflow to the workspace root.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -2872,7 +2888,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -2948,7 +2964,7 @@
|
||||
"format": "date-time"
|
||||
},
|
||||
"state": {
|
||||
"description": "Deployed workflow graph snapshot pinned by this version. Credential-bearing values are redacted: `oauth-input`, `password: true`, and table sub-block values are null; sensitive nested tool parameters and every parameter without authoritative codec metadata are null.",
|
||||
"description": "Deployed workflow graph snapshot pinned by this version, with credential-bearing values redacted to null: `oauth-input`, `password: true`, table sub-block values, sensitive nested tool parameters, and any parameter without authoritative codec metadata.",
|
||||
"$ref": "#/components/schemas/DeployedWorkflowState"
|
||||
}
|
||||
},
|
||||
@@ -3321,7 +3337,7 @@
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"title": "Deploy result",
|
||||
"description": "Deployment attempt accepted for processing. Activation is asynchronous; `latestDeploymentAttempt` on this response is the attempt handle. The request is NOT idempotent — every POST mints a new deployment version, so a retry after a timeout creates a second version rather than returning the first. `latestDeploymentAttempt` is returned only here: `GET /workflows/{id}` does not carry it, so poll activation with `isDeployed` and `deployedAt` on the workflow, or with `isActive` on `GET /workflows/{id}/versions`."
|
||||
"description": "Deployment attempt accepted for processing. Activation is asynchronous, and `latestDeploymentAttempt` is the attempt handle — returned only here. Poll activation with `isDeployed` and `deployedAt` on the workflow, or `isActive` on `GET /workflows/{id}/versions`."
|
||||
},
|
||||
"DeployWorkflowResponse": {
|
||||
"type": "object",
|
||||
@@ -3394,7 +3410,8 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"UndeployResult": {
|
||||
"type": "object",
|
||||
@@ -3620,7 +3637,8 @@
|
||||
"minimum": 1,
|
||||
"maximum": 2147483647
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"WorkflowExportPayload": {
|
||||
"type": "object",
|
||||
@@ -3670,7 +3688,9 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"type": "string",
|
||||
"description": "Canonical containing-folder path; `/` is the workspace root."
|
||||
"title": "Folder path",
|
||||
"description": "Canonical containing-folder path; `/` is the workspace root.",
|
||||
"maxLength": 4096
|
||||
}
|
||||
},
|
||||
"required": ["id", "name", "description", "workspaceId", "folderPath"],
|
||||
@@ -3748,7 +3768,9 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"type": "string",
|
||||
"description": "Canonical containing-folder path."
|
||||
"title": "Folder path",
|
||||
"description": "Canonical containing-folder path.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
@@ -3825,7 +3847,7 @@
|
||||
},
|
||||
"folderPath": {
|
||||
"description": "Destination folder path; omit for the workspace root.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/FolderPathInput"
|
||||
},
|
||||
"name": {
|
||||
"description": "Override for the imported workflow name.",
|
||||
@@ -3936,7 +3958,7 @@
|
||||
"required": ["runId", "workflowId", "status", "output", "error"],
|
||||
"additionalProperties": false,
|
||||
"title": "Workflow run result",
|
||||
"description": "Synchronous workflow run output and in-band execution status. Run failures are reported in band, not as HTTP errors — a synchronous run that exceeds its execution timeout returns HTTP 200 with `status: \"failed\"` and `error.code: \"TIMEOUT\"`, so always branch on `status` rather than on the HTTP status alone."
|
||||
"description": "Synchronous workflow run output and in-band execution status. Run failures are reported in band, not as HTTP errors — a run that exceeds its execution timeout returns HTTP 200 with `status: \"failed\"` and `error.code: \"TIMEOUT\"`, so branch on `status`."
|
||||
},
|
||||
"ExecuteWorkflowSyncResponse": {
|
||||
"type": "object",
|
||||
@@ -4029,7 +4051,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"executionTimeoutSeconds": {
|
||||
"description": "Requested server-side timeout for an asynchronous run, in seconds. This is an upper bound on the request, not the effective timeout: the run uses the smaller of this value and the account plan's execution timeout, so requesting more than the plan allows silently yields the plan timeout with no warning. Rejected with 400 unless `async` is true.",
|
||||
"description": "Requested server-side timeout for an asynchronous run, in seconds. An upper bound, not the effective timeout: the run uses the smaller of this value and the plan's execution timeout, so requesting more than the plan allows silently yields the plan timeout. Rejected with `400` unless `async` is true.",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 604800
|
||||
@@ -4059,11 +4081,11 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"includeFileBase64": {
|
||||
"description": "Inline eligible output files as base64 content.",
|
||||
"description": "Inline eligible output files as base64 content. Rejected when `async` is true.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"base64MaxBytes": {
|
||||
"description": "Maximum total bytes of file content to inline as base64.",
|
||||
"description": "Maximum total bytes of file content to inline as base64. Rejected when `async` is true.",
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 10485760
|
||||
@@ -4071,7 +4093,7 @@
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"title": "Execute workflow request",
|
||||
"description": "Input and execution-mode options for a deployed workflow. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require `stream: true`. (6) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
|
||||
"description": "Input and execution-mode options for a deployed workflow. Each option carries the modes it requires and the modes that reject it; a violated combination is a 400.",
|
||||
"examples": [
|
||||
{
|
||||
"input": {
|
||||
@@ -4118,7 +4140,7 @@
|
||||
"failed",
|
||||
"cancelled"
|
||||
],
|
||||
"description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. The status alone does not say which. On the single-run response `paused.automaticResumeWaitingReason` distinguishes them: it is recorded whenever a resume attempt fails and cleared once a resume succeeds, so a null value means the run is waiting on human input. When the failure is not retryable or the automatic retries are exhausted, the reason is prefixed `Automatic resume requires manual intervention: `. Run-list items carry no `paused` object, so the two cases are indistinguishable there."
|
||||
"description": "Current or terminal run status. `redacting` is transient, reported while a finished run's output is being scrubbed. `paused` means the run is waiting to be resumed — either held at a human-in-the-loop pause point, or left paused by a resume attempt that did not complete. Only the single-run response distinguishes the two, through `paused.automaticResumeWaitingReason`."
|
||||
},
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
@@ -4205,7 +4227,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"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"],
|
||||
@@ -4259,7 +4281,7 @@
|
||||
"cancelled",
|
||||
"queued"
|
||||
],
|
||||
"description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. The status alone does not say which. On the single-run response `paused.automaticResumeWaitingReason` distinguishes them: it is recorded whenever a resume attempt fails and cleared once a resume succeeds, so a null value means the run is waiting on human input. When the failure is not retryable or the automatic retries are exhausted, the reason is prefixed `Automatic resume requires manual intervention: `. Run-list items carry no `paused` object, so the two cases are indistinguishable there."
|
||||
"description": "Current or terminal run status. `redacting` is transient, reported while a finished run's output is being scrubbed. `paused` means the run is waiting to be resumed — either held at a human-in-the-loop pause point, or left paused by a resume attempt that did not complete. Only the single-run response distinguishes the two, through `paused.automaticResumeWaitingReason`."
|
||||
},
|
||||
"trigger": {
|
||||
"anyOf": [
|
||||
@@ -4374,7 +4396,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Reason automatic resume is waiting, or null when it is not waiting."
|
||||
"description": "Why automatic resume is waiting, or null when it is not — on a paused run, null means it is waiting on human input. Recorded whenever a resume attempt fails and cleared once one succeeds. A non-retryable or exhausted failure is prefixed `Automatic resume requires manual intervention: `."
|
||||
},
|
||||
"pausePointCount": {
|
||||
"type": "number",
|
||||
@@ -4650,7 +4672,7 @@
|
||||
"description": "Whether a paused execution was cancelled."
|
||||
},
|
||||
"reason": {
|
||||
"description": "Machine-readable cancellation outcome. Present on every cancellation, including full successes — it is not a partial-failure marker. `recorded` means cancellation was durably recorded (the normal success value). `redis_unavailable` and `redis_write_failed` mean the distributed cancellation signal could not be written, so an already-running execution may not observe the cancellation. `paused_event_publish_failed` and `paused_database_cancel_failed` name the failing step when cancelling a paused human-in-the-loop run.",
|
||||
"description": "Machine-readable cancellation outcome, present on every cancellation including full successes. `recorded` is the success value. `redis_unavailable` and `redis_write_failed` mean the distributed cancellation signal was not written, so an already-running execution may not observe the cancellation. `paused_event_publish_failed` and `paused_database_cancel_failed` name the failing step for a paused run.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"recorded",
|
||||
@@ -4671,7 +4693,7 @@
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"title": "Cancel workflow run result",
|
||||
"description": "Outcome of a workflow run cancellation request. Cancelling a run that has already reached a terminal state (completed, failed, or cancelled) succeeds with no effect rather than returning an error — treat this endpoint as best-effort and poll the run to observe the final state."
|
||||
"description": "Outcome of a workflow run cancellation request. Cancellation is best-effort: a run already in a terminal state succeeds with no effect, so poll the run to observe its final state."
|
||||
},
|
||||
"CancelWorkflowRunResponse": {
|
||||
"type": "object",
|
||||
@@ -4708,11 +4730,15 @@
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Canonical folder path used as the public folder identifier."
|
||||
"title": "Non-root folder path",
|
||||
"description": "Canonical folder path used as the public folder identifier.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"parentPath": {
|
||||
"type": "string",
|
||||
"description": "Canonical parent path; `/` is the root."
|
||||
"title": "Folder path",
|
||||
"description": "Canonical parent path; `/` is the root.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
@@ -4753,7 +4779,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
|
||||
"description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
|
||||
}
|
||||
},
|
||||
"required": ["data", "nextCursor"],
|
||||
@@ -4801,6 +4827,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"NonRootFolderPathInput": {
|
||||
"title": "Non-root folder path input",
|
||||
"description": "Non-root folder path. A missing leading slash is normalized before validation. 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": "string"
|
||||
},
|
||||
"CreateWorkflowFolderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4811,7 +4843,7 @@
|
||||
},
|
||||
"path": {
|
||||
"description": "Path of the folder to create.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "path"],
|
||||
@@ -4854,11 +4886,11 @@
|
||||
},
|
||||
"path": {
|
||||
"description": "Current folder path.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
},
|
||||
"destinationPath": {
|
||||
"description": "New full path for the folder and its descendants.",
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/NonRootFolderPathInput"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "path", "destinationPath"],
|
||||
@@ -4871,7 +4903,9 @@
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path of the deleted workflow folder."
|
||||
"title": "Folder path",
|
||||
"description": "Path of the deleted workflow folder.",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"deleted": {
|
||||
"type": "boolean",
|
||||
|
||||
Reference in New Issue
Block a user