# SteadyDo REST API > A personal tracker for repeatable processes, Orders, standalone tasks, and company context. Release: 1.0.0-draft (2026-09-19). OpenAPI is normative; llms.txt is a community discovery convenience, not an authentication mechanism or tool protocol. Use Authorization: Bearer YOUR_STEADYDO_API_KEY. Inspect capabilities first. Honor scopes, strong ETags, idempotency keys, Retry-After, and destructive previews. Never disclose keys. ## Reference - [OpenAPI JSON](/api/openapi.json) - [OpenAPI YAML](/api/openapi.yaml) - [Interactive reference](/api/docs) - [Operation index](/api/operation-index.md) - [Quickstart](/api/docs/quickstart) - [Agent guide](/api/docs/agent-guide) - [Authentication](/api/docs/authentication) - [Errors](/api/docs/errors) - [Changelog](/api/docs/changelog) - [Compatibility](/api/docs/compatibility) - [Support](/api/docs/support) ## Workflows - [manage-standalone-work](/api/docs/workflows/manage-standalone-work) - [companies](/api/docs/companies) - [processes-and-items](/api/docs/processes-and-items) - [fields-and-views](/api/docs/fields-and-views) - [maintain-template](/api/docs/workflows/maintain-template) - [workflow-states](/api/docs/workflow-states) - [create-operate-order](/api/docs/workflows/create-operate-order) - [rotate-agent-key](/api/docs/workflows/rotate-agent-key) - [archive-recover](/api/docs/workflows/archive-recover) - [work-next-up](/api/docs/workflows/work-next-up) - [export](/api/docs/export) - [bootstrap-workspace](/api/docs/workflows/bootstrap-workspace) # Activity and history History is a read-only record of product mutations. Use `listOrderActivity`, `listProcessItemActivity`, `listRecentActivity`, `listCompletedActivity` and `getWeeklyActivity` as applicable. Never POST or PATCH an activity row; perform the canonical task/item/order/configuration action instead. Reads are newest-first by occurredAt with a deterministic, opaque tie order. Preserve the cursor and filters; do not reconstruct cursors from public IDs. Orders embed at most50 entries and expose activityNextCursor for the order-history endpoint. Global and weekly projections are bounded independently; see [pagination](/api/docs/pagination). A product action may create a resource event (creation, edits, lifecycle/workflow transitions, template application, division) and a separate API audit record. These are different records: a read, a preview, an idempotent replay or a rejected request must not be interpreted as a fresh product change. Some configuration actions update version/configuration without adding a resource-history row; history is not a complete database changefeed. Follow returned state/version and request ID rather than requiring one activity per HTTP call. Public actor metadata is `{type:session|api_key|unknown,id?,requestId?,operation?}`. The ID is an opaque user/key identifier, never its secret; legacy events without attribution return unknown. Revocation does not rewrite historical attribution. Correlation IDs carry no authority; see [request correlation](/api/docs/request-correlation). Resource activities currently have no automatic age-based expiry. They remain until supported resource/workspace deletion removes them; process deletion can retain read-only snapshot/history according to its preview. This is not an indefinite-storage SLA. API operational audit events have a separate90-day maintenance policy and are not part of the public resource activity collection. Idempotency records expire after24hours and are not history. Export includes retained public resource history, not operational security logs. --- # Agent operating guide 1. Call `getCapabilities` and `getWorkspaceSummary` before planning mutations. 2. Follow `allowedActions` and typed links; never invent endpoint names or parse opaque IDs/cursors. 3. Read the canonical resource before a write. Send the target version required by that operation in `If-Match`; nested workflow operations use the nested task/stage version. See [concurrency](/api/docs/concurrency). On 412 reread, reconcile, and use a fresh mutation key. 4. Send an idempotency key for creates, duplicates, divides, scoped template applies, bulk actions, and destructive changes. 5. Preview non-obvious or destructive effects. Confirmation tokens expire after ten minutes, are single-use, and are bound to actor, API key, operation, resource, aggregate version, and requested scope. 6. Treat templates as future-item defaults. Never imply that editing a template changed existing items; use preview and scoped apply. 7. Treat company order defaults as future-order defaults. 8. Mutate canonical tasks/items/orders, then reread computed Home, Next Up, Attention, or Completed projections. 9. Stop for user intent before permanent deletion or materially broad template application. 10. Never disclose, log, echo, or persist API keys. All scopes listed on an operation are conjunctive. Key creation/rotation can preserve or narrow the caller's scopes, never widen them; successor expiry cannot outlive the caller. --- # Authentication and API keys Send `Authorization: Bearer YOUR_STEADYDO_API_KEY` over HTTPS. Never send a key in a URL, source control, logs, screenshots, third-party prompts, or client-visible analytics. Keys have scopes, optional expiry, revocation state, safe prefix/fingerprint, and debounced last-use metadata. A full-access key holds `workspace:read`, `workspace:write`, `config:write`, `archive:write`, `destructive:write`, and `keys:manage`. Required scopes are conjunctive. `createApiKey` and `rotateApiKey` return a secret once. A successor may preserve or narrow the caller's scopes and expires no later than the caller. Validate the replacement before revoking the only usable key. Last-use timestamps are approximate because writes are debounced. 401 covers missing, malformed, invalid, expired, or revoked credentials without confirming key existence. 403 means a valid key lacks a required scope. Unknown and wrong-owner resources return the same non-enumerating 404. | Scope | Permission and risk | | --- | --- | | `workspace:read` | Read business records, history, queues and exports. Treat exported/customer data as confidential. | | `workspace:write` | Create and edit operational work, complete/reopen tasks and change saved timezone preferences. Writes can alter schedules and queues. | | `config:write` | Change process, field, view, template and company configuration. Defaults and required fields affect other work and future creations. | | `archive:write` | Archive or restore records. Archiving hides work from active queues but does not permanently erase it. | | `destructive:write` | Apply irreversible deletion after the required preview; retain only for an agent explicitly authorized to destroy data. | | `keys:manage` | Create, rotate or revoke credentials. Delegation cannot exceed the caller, but a leaked key can still issue successors within those limits. | Begin read-only. Add only the scopes needed for a particular workflow and inspect [capabilities](/api/docs/workflows/bootstrap-workspace) before each new kind of action. A `403 insufficient_scope` problem may return `requiredScopes`; every listed scope is required. A wider scope never changes the authenticated owner. --- # Companies A company is lightweight context shared by work, not a CRM pipeline or owner boundary. Standalone tasks, process items and Orders can reference an owned company ID. Related IDs must belong to the same workspace. Name/code/contact/payment/notes properties are ordinary product context; customFields use configured keys and typed values. Read company field definitions/card visibility before constructing a replacement custom-value map. Company fields support the schema's nine types with required presence, select options, finite numbers/currency, dates, boolean and owned company references. Table/card visibility does not make a required field optional. A false checkbox is a valid configured value. Company configuration and associations are versioned; send the documented company/configuration ETag and idempotency key. **Archiving a company does not archive related tasks, process items or Orders.** The company leaves active pickers but existing work retains its association. Restore the company to make it active again. Do not emulate archival by deleting relationships. A company future-order template must be an owned, published template in the Orders process, either unscoped or scoped to that company. An explicit order template wins; otherwise company default wins; otherwise the built-in Orders workflow is used. These settings affect future orders. Existing orders retain independent workflow copies. Editing the company's private template likewise does not retroactively change existing orders. Permanent company deletion is separate from archive and requires its preview/confirmation and destructive scope. Inspect how links and private future-order configuration will be cleared; related work is not implicitly permission to delete that work. Use returned impact, not guessed cascade behavior. See [archive/recover](/api/docs/workflows/archive-recover), [fields](/api/docs/fields-and-views), and [order workflow](/api/docs/workflows/create-operate-order). --- # Optimistic concurrency Resources expose integer `version` values and strong ETags such as `"v3"`. Send the version of the operation's target in `If-Match`. Top-level updates use that resource's GET ETag. Nested workflow task actions use the task version from the parent representation; stage actions use the stage version. Creating a task uses its containing stage version; creating a stage uses its order/item version. Updating or hiding a process field uses its field version; creating/reordering fields uses the process version. Read the documented target rather than substituting the parent ETag for every nested operation. A stale write returns `412 version_conflict` without applying the mutation. Reread the parent representation, compare current state with intended changes, then use the correct fresh target ETag and a new idempotency key for the reconciled mutation. A changed If-Match changes the normalized request. For a lost response, resend the exact original request, ETag, body, and key to recover its committed outcome. Batch application binds all versions through its preview token and has no separate If-Match header. For an owned resource, a stale-version problem can include `currentVersion` and its `ETag` response header. These are useful for displaying the conflict, but are not a substitute for reading the changed fields before reconciliation. They are omitted where the resource or target cannot safely be identified. Example: a write with `If-Match: "v2"` against version 3 returns `412`, `ETag: "v3"`, and a problem containing `code: "version_conflict"` and `currentVersion: 3`. This response has not applied the stale change. Company field configuration has an empty virtual version 0 before its first write. `GET /company-fields` returns `ETag: "v0"`; use that value and `expectedVersion: 0` for the first replacement. The saved configuration advances to version 1. Existing resources reject an inapplicable version 0 with `412 version_conflict`. For collection reorders, `/tasks/reorder` uses the first task in `taskIds` as its `If-Match` target, and `/processes/reorder` uses the first process in `itemIds`. `/orders/reorder` uses the moving `orderId`. `/processes/{processId}/items/reorder` uses the containing process version. Nested workflow task reorders use the containing stage version. Task, process, item, and workflow task reorder bodies must include `expectedVersions` for every submitted member ID; the map protects each member even when the containing resource version has not changed. A missing member version is rejected; a stale member version returns `412` without changing positions. Creating a process item uses the containing process version. All standalone task checklist mutations use the containing task version. Process field deletion preview and permanent deletion use the process version; updating or hiding a field uses that field's version. Template actions, including preview/apply to items and setting a default, use the template version; creating a template uses the process version. The response version may belong to the created or returned resource, so reread the documented target before another operation. --- # Content types and encoding Successful reads and existing-resource actions use `200`; documented creations use `201` and return their resource envelope. Preserve a returned `Location` to rediscover a created resource and `ETag` for the documented concurrency target. No v1 operation currently starts an asynchronous `202` job or declares `204` success. Clients must follow the individual response schema instead of inferring status from the HTTP method. API responses expose `API-Version`, `X-Request-Id`, and `X-API-Contract-SHA256`. The last value is the SHA-256 of the complete public OpenAPI JSON bytes expected by the backend; compare it with a freshly downloaded contract to detect a backend/documentation release mismatch. It is a compatibility fingerprint, not authentication. Authenticated resources use `Cache-Control: no-store, no-transform`; public documentation has a separate revalidation policy. Rate-limit headers are present after quota evaluation; an early authentication or parsing failure may not include them. `Retry-After` applies when throttling or temporary readiness requests a delay. Conditional future deprecation headers follow the [compatibility policy](/api/docs/compatibility). JSON bodies use UTF-8 with `Content-Type: application/json`; clients request JSON with `Accept: application/json`. Errors use `application/problem+json`. Workspace export v1 also returns JSON, page by page; it does not offer CSV or a binary download. Malformed JSON or invalid query syntax returns 400. Unsupported request media returns 415. A write body larger than the advertised limit returns 413. Current operations return their documented 200 or 201 envelope, including deletes and actions. Do not assume a successful delete returns 204. A future operation explicitly declaring 204 would have no JSON body. --- # Dates, times, and timezones Dates use `YYYY-MM-DD`; times use 24-hour `HH:mm`; instants use RFC3339 with an explicit offset and responses normalize instants to UTC. A date has no UTC offset. Never send `17/08/2026` or parse an opaque date using the caller's locale. `getPreferences` / `updatePreferences` read and set the owner's IANA timezone. UTC is the fallback until configured. Queues determine today by converting the current instant into that timezone. A task is overdue when its dueDate is earlier than today; a due time does not turn a date-only overdue comparison into a timestamp comparison. A local dueTime belongs to the task's dueDate/workspace timezone; it is not rewritten when preferences change. Weekly history uses a Monday-inclusive, following-Monday-exclusive local-date interval. For `week=2026-09-20` (Sunday), the interval is September14 through before September21. Each event's occurredAt instant is converted to the workspace timezone before date membership is checked. At `2026-09-20T22:30:00Z`, Europe/Moscow is already Monday September21 while UTC is still Sunday. Do not substitute UTC-midnight filtering for the owner's calendar. Calendar arithmetic adds integer days on a date-only representation, so daylight-saving transitions cannot introduce an hour or move the result to the previous date. Order estimated arrival is plannedShipmentDate plus deliveryDays (default45). For October1 plus45 the result is November15. No planned shipment means null arrival. Working-day deadlines count Monday-Friday, starting after the start date, without a holiday calendar: Friday September18 plus one working day is Monday September21; zero working days returns the start date. Stage activation and manual schedule inputs can change derived deadlines; reread after a mutation. Shipment-relative workflow due dates add the signed offset to shipment date (October1 with -4 is September27). Stage-deadline tasks inherit the stage deadline. Weekly order-task completion advances an existing due date by seven days while the next occurrence is no later than shipment plus weeklyStopOffsetDays (default -10); without a usable next occurrence/stop boundary the task finishes. Manual dates remain explicit inputs. Process-item recurrence accepts only its documented stopOffsetDays shape, not an arbitrary recurrence language. Consult each workflow schema because instance and Orders schedule names differ. --- # Documentation maintenance and accessibility ## Deliberate Scalar updates A dependency change needs a contract/security reviewer and release-note reviewer from the ownership policy. Record the old/new exact Scalar and transitive version set, upstream release/security notes, license changes and lockfile integrity. Do not use a floating CDN or automatically accept an upgrade. Run pinned OpenAPI standards/example validation, link/freshness checks and typecheck; then build the actual Worker and run documentation production browsers. Exercise entered synthetic bearer credentials, intended API request, storage/cookies/IndexedDB/reload, all outbound origins, search/tag/schema/example/download controls, keyboard navigation, reduced motion, narrow/zoomed view and failed-load recovery. Inspect CSP violations and the styling exception in [architecture](/api/docs/architecture). Retain current receipts before merging. Roll back the package/lockfile together if privacy or rendering regresses. ## Manual keyboard and screen-reader smoke This is a repeatable procedure; publication of it is not evidence that a human completed it. Use a clean browser profile, no saved session and a synthetic disposable key. Record OS/browser/assistive-technology versions and viewport/zoom. For keyboard-only review, Tab from the address bar: the skip link must be visible, Enter must focus the API reference landmark, every interactive control must have visible focus, and no focus trap may occur outside an open dialog. Search for createTask, follow its tag/operation, expand schema/examples, open and close the request editor, use raw-spec links and return. Escape must close dismissible overlays with focus restored. With NVDA/Firefox or VoiceOver/Safari, navigate landmarks and headings. Confirm operation names/methods, parameter requiredness, schema expand/collapse state, code-copy labels, error feedback and auth input names are announced. Ensure links make sense outside paragraph context. Enter a synthetic key only into the auth control and never record its value. Check text at200%zoom and reflow at320CSSpixels, reduced motion and high contrast. Verify that failed specification loading announces an alert with raw-spec/support recovery links. Report each defect with route, action, expected/actual announcement or focus order and a redacted screenshot. Record pass/fail per step, not simply 'accessible'. A new human quickstart test separately verifies account/key setup without undocumented assistance. ## Historical documentation Only v1 draft is currently published; there is no supported historical stable major to fabricate. On the first supported stable release, archive immutable JSON/YAML, generated guides, release commit/digest and changelog under a version-specific artifact location. Keep each supported major's docs available while its API major is supported, with canonical links to that major and an explicit latest-version link. When support ends, keep historical artifacts read-only, label the end-of-support date prominently and disable interactive execution against unsupported endpoints. Never silently redirect a historical contract URL to a different major. Redact exposed secrets through incident procedure, retaining an audit note rather than rewriting history invisibly. Test supported/archived routes and metadata whenever introducing another major. --- # Error recovery catalog Errors use `application/problem+json`. Match `status` and stable `code`; `detail` is explanatory text, not a parsing contract. The examples below use fictional request IDs and show representative responses. Every response is private and uncached. See the [error registry](/api/error-codes.yaml), [retry rules](/api/docs/retries), and [operation index](/api/operation-index.md). ## 400 — Malformed requests Example operation: [createTask](/api/openapi.json#/paths/~1tasks/post) (POST /tasks). Missing JSON, malformed JSON, unsupported query names/values, or missing Idempotency-Key/If-Match fail protocol validation. Supply an object body and the operation’s required headers. For example, replace a missing create-task body with {"title":"Review delivery"}. Do not retry the same malformed request. ```http HTTP/1.1 400 Bad Request Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example {"type":"https://steadydo.app/problems/invalid_request","title":"invalid request","status":400,"detail":"A JSON request body is required","code":"invalid_request","requestId":"req_error_example"} ``` ## 401 — Authentication failures Example operation: [getCapabilities](/api/openapi.json#/paths/~1capabilities/get) (GET /capabilities). Missing or malformed Authorization uses authentication_required; invalid, expired and revoked keys use invalid_api_key without revealing whether a key exists. Supply Authorization: Bearer followed by your current key. After rotation use the successor. Retry only after correcting credentials; never put a key in a URL or support report. ```http HTTP/1.1 401 Unauthorized Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example {"type":"https://steadydo.app/problems/invalid_api_key","title":"invalid api key","status":401,"detail":"The API key is invalid, expired, or revoked","code":"invalid_api_key","requestId":"req_error_example"} ``` ## 403 — Scope failures Example operation: [createTask](/api/openapi.json#/paths/~1tasks/post) (POST /tasks). The key authenticated but lacks the required scope. Check the operation’s x-scopes and getCapabilities; obtain an appropriately authorized key. A wrong-owner resource still receives 404 rather than revealing it exists. Retry only after authorization changes. ```http HTTP/1.1 403 Forbidden Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example {"type":"https://steadydo.app/problems/insufficient_scope","title":"insufficient scope","status":403,"detail":"The API key lacks a required scope","code":"insufficient_scope","requestId":"req_error_example"} ``` ## 404 — Unknown or hidden resources Example operation: [getTask](/api/openapi.json#/paths/~1tasks~1{taskId}/get) (GET /tasks/{taskId}). Unknown IDs, wrong owners and mismatched parents deliberately share not-found behavior. Rediscover IDs through an authorized collection and check the route’s resource type and parent. Never probe other owners or infer existence from this response. Retry only with a corrected route/ID or restored visibility. ```http HTTP/1.1 404 Not Found Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example {"type":"https://steadydo.app/problems/resource_not_found","title":"resource not found","status":404,"detail":"Resource not found","code":"resource_not_found","requestId":"req_error_example"} ``` ## 409 — State and idempotency conflicts Example operation: [completeTask](/api/openapi.json#/paths/~1tasks~1{taskId}~1complete/post) (POST /tasks/{taskId}/complete). An invalid lifecycle transition, incompatible default, or reorder state can conflict. Read the resource and choose a permitted action; do not blindly repeat. A reused idempotency key with a different request instead uses idempotency_conflict: resend the exact original request, or use a fresh key for a genuinely new intent. ```http HTTP/1.1 409 Conflict Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example {"type":"https://steadydo.app/problems/state_conflict","title":"state conflict","status":409,"detail":"The operation conflicts with resource state","code":"state_conflict","requestId":"req_error_example"} ``` ## 412 — Version conflicts Example operation: [updateTask](/api/openapi.json#/paths/~1tasks~1{taskId}/patch) (PATCH /tasks/{taskId}). If-Match is stale. No update was applied. When the owned target can be identified, the error includes its current ETag and `currentVersion`. GET the target to inspect the changed fields before using that version; metadata alone is not reconciliation. Retry the reconciled intent with the fresh ETag and a new idempotency key. Nested workflow actions use the target task/stage version, not an arbitrary parent version. The [executable conflict workflow](/api/docs/workflows/recover-conflicts) asserts stale rejection, retained state, exact replay, changed-payload conflict and successful reconciliation. ```http HTTP/1.1 412 Precondition Failed Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example ETag: "v3" {"type":"https://steadydo.app/problems/version_conflict","title":"version conflict","status":412,"detail":"The resource version has changed","code":"version_conflict","requestId":"req_error_example","currentVersion":3} ``` ## 413 — Request size limits Example operation: [createTask](/api/openapi.json#/paths/~1tasks/post) (POST /tasks). The body exceeds 256 KiB, whether declared by Content-Length or received while streaming. Reduce the request below the byte limit; do not merely change Content-Length. Retry only after reducing the body. Resource/atomic-scope limits use 422 instead. ```http HTTP/1.1 413 Content Too Large Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example {"type":"https://steadydo.app/problems/payload_too_large","title":"payload too large","status":413,"detail":"The request body exceeds 256 KiB","code":"payload_too_large","requestId":"req_error_example"} ``` ## 415 — Content type Example operation: [createTask](/api/openapi.json#/paths/~1tasks/post) (POST /tasks). A body with an unsupported media type or charset is rejected. Send Content-Type: application/json (optionally charset=utf-8) and valid UTF-8 JSON. Retry after correcting the header and encoding; do not resend text/plain or form data. ```http HTTP/1.1 415 Unsupported Media Type Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example {"type":"https://steadydo.app/problems/unsupported_media_type","title":"unsupported media type","status":415,"detail":"Send UTF-8 application/json","code":"unsupported_media_type","requestId":"req_error_example"} ``` ## 422 — Field and relationship validation Example operation: [createTask](/api/openapi.json#/paths/~1tasks/post) (POST /tasks). JSON is syntactically valid but violates a field/schema or relationship rule. A blank title can be corrected to {"title":"Review delivery"}; paths such as /title in detail identify the invalid field. Current responses report validation details in detail; clients must not require a violations array. Unknown or wrong-owner related IDs receive 404. confirmation_expired requires a new preview; atomic_scope_too_large requires a smaller scope (1,000 records and 2 MiB maximum), with no partial changes. Retry only after correcting input or refreshing the preview. ```http HTTP/1.1 422 Unprocessable Content Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example {"type":"https://steadydo.app/problems/validation_failed","title":"validation failed","status":422,"detail":"/title must NOT have fewer than 1 characters","code":"validation_failed","requestId":"req_error_example"} ``` ## 429 — Throttling Example operation: [getCapabilities](/api/openapi.json#/paths/~1capabilities/get) (GET /capabilities). Respect Retry-After in seconds before retrying. RateLimit-Remaining is zero; RateLimit-Reset is the reset instant in Unix seconds. In this example wait at least 60 seconds, then add random jitter. Retry reads or an identical write using its original idempotency key; use bounded backoff and stop after five attempts. Persistent throttling requires reducing traffic, not cycling keys. ```http HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example Retry-After: 60 RateLimit-Limit: 600 RateLimit-Remaining: 0 RateLimit-Reset: 1789834860 {"type":"https://steadydo.app/problems/rate_limited","title":"rate limited","status":429,"detail":"The rate limit was exceeded","code":"rate_limited","requestId":"req_error_example"} ``` ## 500 — Internal failures Example operation: [createTask](/api/openapi.json#/paths/~1tasks/post) (POST /tasks). An unexpected server failure does not prove whether a write committed. Preserve X-Request-Id. Retry a safe read with backoff; for a write whose outcome is unknown, resend exactly the same request with its original idempotency key before attempting another mutation. Stop after five attempts and report the request ID, UTC time and operation through the configured support channel, without credentials. ```http HTTP/1.1 500 Internal Server Error Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example {"type":"https://steadydo.app/problems/internal_error","title":"internal error","status":500,"detail":"The request could not be completed","code":"internal_error","requestId":"req_error_example"} ``` ## 503 — Temporary service failures Example operation: [getHome](/api/openapi.json#/paths/~1home/get) (GET /home). Home or other materialized reads can return this while a legacy projection prepares; no partial counts are returned. Honor Retry-After (one second here), apply jitter and bounded backoff, and stop after five attempts if readiness does not recover. For other service failures preserve the request ID and use the same safe/idempotent recovery rules as 500. ```http HTTP/1.1 503 Service Unavailable Content-Type: application/problem+json Cache-Control: no-store, no-transform X-Request-Id: req_error_example Retry-After: 1 {"type":"https://steadydo.app/problems/service_unavailable","title":"service unavailable","status":503,"detail":"Workspace projections are being prepared. Retry shortly.","code":"service_unavailable","requestId":"req_error_example"} ``` --- # Workspace export `exportWorkspace` is **beta**. Its product-level data coverage is intentional, but record-type shape and operational limits may change before the first stable release. Pin the API version, check the published contract and changelog, and validate each export before relying on it. Beta does not relax authentication, ownership or secret-redaction guarantees. Export is a paginated JSON response, not an asynchronous job, downloadable archive or ordinary workspace summary. Start `GET /workspace/export?limit=50`, retain each data.items page and follow data.nextCursor until data.complete is true. An empty page can still have a continuation. Cursors are opaque and tied to owner/query; restart after invalid_request. The first page includes timezone preferences. Included content covers saved preferences, companies/company field configuration, standalone tasks/checklists, processes/fields, templates, process items, Orders, copied workflow stages/tasks, resource activities and retained deleted-process snapshots. Each entry identifies its resource type. Public IDs and approved product fields are included; internal database IDs/owner identifiers, API-key secrets/hashes, operational audit events, confirmation tokens, rate counters and idempotency cache records are excluded. Check the current response schema for exact record types. The server streams bounded pages on demand and does not store a server-side export file. Retention and protection of any downloaded copy are the caller's responsibility. No cross-request snapshot isolation is promised: concurrent writes can change later pages. For a consistency-sensitive backup, pause writes operationally, finish all pages and verify relationships/counts. Export alone is not a supported infrastructure restore procedure. Each entry is `{id,type,value}`. `id` identifies the export record, while `value.publicId` is the stable product ID to match related records. `userPreferences` is the exception: its value contains only `timeZone`. Record timestamps such as `createdAt`, `completedAt` and `occurredAt` are epoch milliseconds in export v1, unlike RFC 3339 timestamps in resource DTOs. Date-only fields remain `YYYY-MM-DD`. Read the explicit per-type value schema; do not apply a task DTO parser to a raw export record. Missing optional fields are absent, not automatically null. Template snapshots and deleted-process records have their own allowlisted nested shapes; arbitrary JSON is permitted only for documented user-authored fields, views and activity details. Page limit is at most 100. Export reads use bounded native pages; this GET operation does not advertise a response-size 413. Request-body size limits belong to write operations. Do not mark an export complete unless `data.complete` is true. On a transient service failure honor Retry-After if returned; restart cursor traversal if its binding is no longer valid. See [pagination](/api/docs/pagination) and [errors](/api/docs/errors). --- # Process fields and views A process field has an opaque id (used in the field route) and a key (used in an item's fieldValues). Discover both from the process/field response; never assume they are equal. Supported public types are text, long_text, number, currency, date, boolean, select, company and url. Boolean values are true/false, not strings; false and numeric zero satisfy required presence. Currency/number values must be finite JSON numbers. Dates are calendar dates, select values must be configured options, company values are owned company IDs, and URLs must use HTTP(S). Unknown keys/unsupported properties are rejected. Blank/whitespace-only required text is invalid. No implicit business default is invented for a missing required custom field. Creation validates required values in the same transaction as the new resource. Existing legacy values whose definitions were removed may remain unchanged; this compatibility allowance does not authorize new unknown values. Treat fieldValues as the documented map replacement and preserve values you intend to keep. Visibility flags independently control create/edit/table/dashboard presentation and filtering. A required, non-hidden field is always shown on create: create/update normalize visibleOnCreate to true, including partial updates that make it required or unhide it. Legacy contradictory rows remain readable and both creation forms expose their required inputs. A hidden field is not required for new creation. Company field table visibility is a different setting and never exempts a required company field. **Hide** removes a field from ordinary presentation without deleting its stored item values. **Permanent field removal** uses preview plus confirmation and removes that field's data/references from affected resources. Inspect impact and returned counts; irreversible removal is not an ordinary PATCH. Built-in Orders fields cannot be deleted. Renaming keys or changing types needs explicit consideration of existing values; do not assume automatic value conversion. Use createProcessField, updateProcessField, reorderProcessFields and hideProcessField; previewProcessFieldDeletion/permanentlyDeleteProcessField perform destructive removal. Creating/reordering/deleting uses the process ETag as documented; updating/hiding uses the field version. Reorder includes the exact configured field set, including hidden fields. updateProcessViews stores the supported table/dashboard/detail settings. Table columns, order, widths, search/filter/sort fields and pageSettings are configuration; they do not change item data. JSON page settings can contain nested objects, arrays, scalar values and null, but no non-JSON runtime values. Use explicit visible field IDs/keys discovered from the process and read configuration back after editing. Company field definitions use explicit preservation semantics. Replacing or removing a key in PUT /company-fields changes future validation and presentation only; it never renames or deletes stored company values. Renaming a key means introducing a new field key. Existing values under removed keys may be submitted unchanged, but new or changed undefined values are rejected. To migrate, configure the new key, then explicitly PATCH each company's customFields replacement map with the intended copied value. To delete retained values, explicitly omit them from that replacement map. Changing a type requires explicitly correcting incompatible values; no automatic coercion occurs. Required flags apply regardless of table visibility. --- # Filtering, search, and sorting Collection operations explicitly list supported filters. Common filters include lifecycle status, archived state, company, process, due-date range, attention state, completion period, and text search. Omission selects the documented default; an unknown filter or sort returns 400 rather than being silently ignored. Task, order and process-item lists default to non-archived records (active and completed), ascending saved `position`. `archived=true` selects archived records; it does not add them to the active result. `status` filters completion independently of archive state. Omitted company/date/search filters impose no restriction. Text search uses the operation's supported text fields; it is not a SQL expression or a regular expression. Dates use `dueFrom`/`dueTo` local calendar boundaries; `completedFrom`/`completedTo` are RFC 3339 instants. Invalid dates, unknown sort names and unsupported query keys return 400. Consult the exact collection schema: company, process, template and history lists have different supported filters. Position-changing operations use dedicated reorder actions and current aggregate versions. Do not emulate reorder by patching internal positions. Filters can produce an empty page with a non-null `nextCursor`: bounded reads may examine a window containing no matches. Continue with that cursor and the same filters until `nextCursor` is null. See [pagination](/api/docs/pagination). To move an order workflow task into another stage, call `moveOrderTask` with `{"destinationStageId":"stage_destination","position":0}` and the current task version in `If-Match`. Read the order first to choose an existing destination stage and verify the final stage/task order afterward. Position is zero-based. For a one-step move within the current stage, the alternative body is `{"direction":"up"}` or `{"direction":"down"}`; do not combine direction with destination fields. Orders also support `specNumber`, `company`, `currentStage`, `attention`, and `plannedShipmentDate` sorts. Process items support `title`, `company`, `currentStage`, `nextAction`, and `field:`. Set `locale=en` or `locale=ru` for localized title/stage/task ordering. Sorting is global across every page: text ignores case and accents and orders numeric runs naturally (2 before 10), while numeric fields and timestamps use numeric order. Missing values are last in both directions. Derived sorts tie by ascending position and exact ID; timestamp ties follow position and exact ID in the requested direction. Derived and custom-field indexes are prepared in bounded background batches. An initial request, day change affecting attention, or dependency change can return `503` with `Retry-After`; retry the same request after that delay. Filters may still yield empty continuation pages. Company and process lists default to `status=active`; use `status=archived` or `status=all` for archive discovery. Company `search` matches name case-insensitively. Process template lists accept `status=draft|published|archived|all` (default all) and `companyId` for exact company-specific scope. Omit companyId to see process-wide and company-specific choices together. Filters are bound into cursors; changing a filter requires starting a new traversal. --- # Idempotency Creates, duplicates, divides, applies, bulk changes, and destructive operations require `Idempotency-Key`. Keys contain 16-128 printable ASCII characters and are namespaced by owner, API key, API major version, operation ID, and the supplied key. Committed successful responses are cached for 24 hours. Failed/uncommitted attempts are not cached. The cache never stores a reusable plaintext API-key secret; one-time key creation/rotation responses use a redacted replay-safe representation and require callers to preserve the originally returned secret themselves. Replaying the same normalized request returns the committed outcome; reusing the key with a different payload returns `idempotency_conflict`. After a timeout, retry the exact request with the same key. Do not invent a new key until the prior outcome is known. `API-Execution-Outcome` separates execution from delivery: `committed` means the mutation committed, `read-only` means no mutation was requested, `not-executed` means execution did not begin, and `unknown` means the server could not establish the outcome. A response-validation error after a commit can still carry `committed`; changing the key could duplicate work. On a timeout, missing response, `unknown`, or committed-but-invalid response, preserve the exact original body, path, If-Match and idempotency key and replay them. Keep the request ID for diagnosis. Do not use this header to bypass status, schema or ownership checks. --- # Pagination Paged collections use opaque cursors, a default limit of 50, and maximum 100. Most return `data.items` and `data.nextCursor`. API-key listing preserves its `data` array and returns continuation in `meta.nextCursor`. Follow the response schema for the operation you use. Copy the returned cursor exactly into the next request; never decode or construct it. Stop only when the cursor is null or absent, not when a page happens to contain zero items. Keep filters and the resource path unchanged while continuing. A cursor bound to a different owner, collection, resource, or filter set returns `invalid_request`; restart at the first page. Do not rely on `meta.sort` being present. History is newest-first, with deterministic but opaque ordering for equal timestamps. Order reads embed at most 50 history entries; `data.activityNextCursor` continues at the order activity endpoint. Home returns bounded arrays and `data.pagination` entries for processes, companies, standaloneTasks, nextActions, and recentActivity. Follow each entry's `href` unchanged to continue that section; other sections stay on their first page. Workspace summary uses `data.pagination.processes`. Counts describe the entire workspace, not just the returned page. If a legacy workspace's materialized projection is still being prepared, the API returns `503 service_unavailable` with Retry-After instead of reporting partial counts as complete. Workspace export traverses resource types across multiple requests. It includes saved timezone preferences, then resource records; an empty page with a continuation cursor is valid. `data.complete` is true only on the final page. Export is not a point-in-time snapshot: concurrent writes can change data while you traverse. For a consistency-sensitive backup, pause writes operationally and verify the resulting export before relying on it; this API does not offer a snapshot-isolation guarantee across pages. ```bash # Uses the helpers from /api/docs/workflow-shell. PAGE=$(api_get '/tasks?limit=25') jq '.data.items' <<< "$PAGE" CURSOR=$(jq -r '.data.nextCursor // empty' <<< "$PAGE") while test -n "$CURSOR"; do ENCODED=$(jq -rn --arg value "$CURSOR" '$value|@uri') PAGE=$(api_get "/tasks?limit=25&cursor=$ENCODED") jq '.data.items' <<< "$PAGE" CURSOR=$(jq -r '.data.nextCursor // empty' <<< "$PAGE") done ``` ## First, continuation and final pages This reduced transcript shows pagination control fields; task records are omitted here for readability, and the schema's task example defines their full shape. `CURSOR_FROM_RESPONSE` is an explanatory placeholder, never a cursor to send literally. ```http GET /api/v1/tasks?limit=1 HTTP/1.1 Host: dashboard.steadydo.com Authorization: Bearer YOUR_STEADYDO_API_KEY HTTP/1.1 200 OK Content-Type: application/json ``` Read `data.items` and copy `data.nextCursor`. When non-null, request `/api/v1/tasks?limit=1&cursor=CURSOR_FROM_RESPONSE`, URL-encoding the actual returned string. The final response has `data.nextCursor: null`. An empty intermediate page is legal and does not end traversal. Keep `limit`, path, owner and filters consistent; changing a filter requires starting over without the cursor. An invalid or differently bound cursor returns this problem, without changing any records: ```http HTTP/1.1 400 Bad Request Content-Type: application/problem+json X-Request-Id: req_example Cache-Control: no-store, no-transform {"type":"https://steadydo.app/problems/invalid_request","title":"invalid request","status":400,"detail":"invalid_request","code":"invalid_request","requestId":"req_example"} ``` ## Executable pagination check Run after [manage standalone work](/api/docs/workflows/manage-standalone-work) with the [Node helper](/api/docs/workflow-node). The returned IDs and cursors are real values from your disposable workspace. This verifies first/continuation/final behavior without decoding or manufacturing a cursor, and checks an invalid cursor independently. ```javascript const paginationExtra = (await request('POST', '/tasks', {title:'Pagination second task'}, undefined, 201)).data; const firstPage = await get('/tasks?limit=1'); check(firstPage.data.items.length === 1 && firstPage.data.nextCursor, 'Expected first-page continuation'); const pagedIds = firstPage.data.items.map(row => row.id); let continuation = firstPage.data.nextCursor; while (continuation) { const page = await get(`/tasks?limit=1&cursor=${encodeURIComponent(continuation)}`); pagedIds.push(...page.data.items.map(row => row.id)); continuation = page.data.nextCursor; } check(new Set(pagedIds).size === pagedIds.length && pagedIds.includes(task.id) && pagedIds.includes(paginationExtra.id), 'Pagination duplicated or lost records'); const invalidCursor = await request('GET', '/tasks?limit=1&cursor=not-a-valid-cursor', undefined, undefined, 400); check(invalidCursor.problem.code === 'invalid_request', 'Wrong cursor error'); await action(`/tasks/${paginationExtra.id}/archive`); const pageDelete = await get(`/tasks/${paginationExtra.id}`); const pagePreview = await request('POST', `/tasks/${paginationExtra.id}/delete-preview`, {}, pageDelete.etag); await request('POST', `/tasks/${paginationExtra.id}/permanent-delete`, {confirmationToken:pagePreview.data.confirmationToken}, pageDelete.etag); ``` --- # Processes and tracked items A process defines identity, fields, templates and generated table/dashboard/detail/workflow/history views. Creation returns data.process and the initial draft defaultTemplateId; discover generated IDs from that response. Configure names, icon/color, field definitions and views before creating tracked items. skipProcessSetup records the intentional skip; it does not create completed work. Reorder processes with exact IDs/member versions; Orders is the reserved system process and cannot be archived as an ordinary custom process. Templates are draft, published or archived configuration. Publish an intended structure before selecting it for new items, and set a default explicitly when appropriate. Template edits do not change existing tracked items. New items copy template structure and version into a snapshot; changing one item's title, fields or workflow does not change its source template or another item. Items have independent completion/archive/restore lifecycle. An archived parent process makes its children read-only. Process archive preview names impacted work; archival records which active children were automatically archived. Restoration can restore those children according to its documented option, without silently reopening independently archived work. Permanent process deletion retains the required read-only snapshot/history until the separate final-deletion path is explicitly confirmed; inspect each preview because effects depend on current state. Item duplication preserves the source and creates independent work. Item division creates one copied workflow per requested title and archives the source; it is not quantity allocation. Orders division instead creates a linked full copy without partitioning the source. Neither changes a source template. Use fresh previews and returned IDs; see [division](/api/docs/workflows/divide-duplicate). For fields and table/dashboard/detail configuration see [fields and views](/api/docs/fields-and-views). For copied stage/task actions, schedule effects, progress and history see [workflow states](/api/docs/workflow-states). For explicit template propagation see [maintain template](/api/docs/workflows/maintain-template). --- # Five-minute REST quickstart Create a key in signed-in **API access** with `workspace:read`, `workspace:write`, `archive:write`, and `keys:manage`. Copy its one-time secret to your secret store. This example uses Node.js 22 or later (no packages). The bootstrap key creates a disposable narrower key; the script revokes that disposable key in a `finally` block. Your bootstrap key stays usable. In Bash, read the secret without placing it in history: ```bash read -rsp 'SteadyDo bootstrap API key: ' STEADYDO_API_KEY; echo export STEADYDO_API_KEY export STEADYDO_BASE='https://dashboard.steadydo.com/api/v1' ``` PowerShell users can set these environment variables through a secret manager or a masked prompt. Do not paste secrets into the script, arguments, logs, URLs or screenshots. Save the following as `quickstart.mjs`, then run `node quickstart.mjs`. It calls `getCapabilities`, `getWorkspaceSummary`, `createApiKey`, `createTask`, `getTask`, `completeTask`, `archiveTask` and `revokeApiKey`. It does not automatically retry uncertain writes with a fresh key. ```javascript const base = (process.env.STEADYDO_BASE || 'https://dashboard.steadydo.com/api/v1').replace(/\/$/, ''); const bootstrapKey = process.env.STEADYDO_API_KEY; if (!bootstrapKey) throw new Error('Set STEADYDO_API_KEY securely before running.'); const run = crypto.randomUUID(); async function request(key, method, path, body, etag, intent) { const response = await fetch(base + path, { method, redirect: 'error', headers: { Authorization: 'Bearer ' + key, Accept: 'application/json', ...(body === undefined ? {} : { 'Content-Type': 'application/json', 'Idempotency-Key': run + '-' + intent }), ...(etag ? { 'If-Match': etag } : {}) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }); const envelope = await response.json(); if (!response.ok) throw new Error(method + ' ' + path + ': HTTP ' + response.status + ' ' + (envelope.code || '') + '; requestId=' + (envelope.requestId || 'unknown')); return { data: envelope.data, etag: response.headers.get('etag') }; } const capabilities = await request(bootstrapKey, 'GET', '/capabilities'); for (const scope of ['workspace:read', 'workspace:write', 'archive:write', 'keys:manage']) { if (!capabilities.data.heldScopes.includes(scope)) throw new Error('Missing scope: ' + scope); } await request(bootstrapKey, 'GET', '/workspace/summary'); const issued = await request(bootstrapKey, 'POST', '/api-keys', { label: 'Disposable quickstart ' + run, scopes: ['workspace:read', 'workspace:write', 'archive:write'] }, undefined, 'key-create'); const temporaryId = issued.data.key.id; let temporaryKey = issued.data.secret; if (!temporaryId || !temporaryKey) throw new Error('One-time secret unavailable; inspect safe key metadata before creating another key.'); try { await request(temporaryKey, 'GET', '/capabilities'); const created = await request(temporaryKey, 'POST', '/tasks', { title: 'Review SteadyDo quickstart' }, undefined, 'task-create'); const taskId = created.data.id; for (const action of ['complete', 'archive']) { const current = await request(temporaryKey, 'GET', '/tasks/' + taskId); if (!current.etag) throw new Error('Missing task ETag; stop and inspect.'); await request(temporaryKey, 'POST', '/tasks/' + taskId + '/' + action, {}, current.etag, 'task-' + action); } const verified = await request(temporaryKey, 'GET', '/tasks/' + taskId); if (!verified.data.archived || !verified.data.completedAt) throw new Error('Unexpected final task lifecycle.'); console.log(JSON.stringify({ taskId, completedAt: verified.data.completedAt, archived: verified.data.archived })); } finally { await request(bootstrapKey, 'POST', '/api-keys/' + temporaryId + '/revoke', {}, undefined, 'key-revoke'); temporaryKey = ''; } ``` Expected responses: key and task creation return **201** with `Location`; reads and complete/archive/revoke return **200**. Reads provide `ETag`; each mutation has a unique idempotency intent. Final state is one completed, archived, restorable fictional task and a revoked disposable key. The archive flag controls archive membership; retained completedAt proves prior completion. Do not treat the archived task's status alone as archive membership. No secret is printed. End the shell session by unsetting `STEADYDO_API_KEY` and `STEADYDO_BASE`. A transport timeout can occur after commit. Preserve the original request body, ETag and idempotency key before retrying; do not rerun the whole script to recover an unknown outcome. If key issuance loses its one-time secret, inspect `listApiKeys` using the bootstrap key and revoke that unusable disposable key. A failed final revocation requires retrying it through the still-usable bootstrap key. See [retries](/api/docs/retries), [authentication](/api/docs/authentication), and the curl helpers in [workflow shell](/api/docs/workflow-shell). This quickstart does not delete the archived task. Restore it with `restoreTask`, or intentionally follow [archive/recover](/api/docs/workflows/archive-recover) for previewed permanent cleanup. Test CI executes this exact JavaScript block against a disposable HTTP router and cleans its fixture through public lifecycle operations; local execution is not production certification. --- # Rate limiting Limits are rolling per-minute buckets, independently counted for each API key and its owner: 600 reads, 180 writes, 30 destructive operations per key; owner-wide thresholds are three times each corresponding limit. Multiple keys do not bypass the owner limit. See [limits](/api/docs/limits) and `getCapabilities`; a lower advertised safety limit takes precedence. A throttled request returns `429 rate_limited`, `Retry-After` seconds and `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (Unix seconds). The response did not apply the requested mutation. Wait at least Retry-After; do not assume remaining capacity in another bucket authorizes the same request. For attempt n starting at zero, wait `max(Retry-After, min(60, 2^n)) + random(0,1)` seconds. Stop after five attempts and surface the request ID. Retry GETs or the exact original idempotent write: same body, ETag and key. A timeout may have committed, so preserve that identity. Throttling is not an account quota or a configuration error. `403 insufficient_scope` requires authorization, `422 validation_failed` requires corrected input, and `422 atomic_scope_too_large` requires a smaller resource scope. None is fixed by waiting or cycling credentials. `503 service_unavailable` may mean a bounded projection/index is preparing; honor its Retry-After. Never automatically broaden a destructive scope. See the complete [429 HTTP example](/api/docs/errors#429--throttling) and [retry matrix](/api/docs/retries). --- # Request correlation Clients may send `X-Request-Id`; otherwise SteadyDo creates one. The server returns it in `X-Request-Id`, problem bodies, and related history/audit events. Use a unique opaque value without personal or secret data. A request ID helps support correlate events. It is not an idempotency key, credential, receipt, or proof that a timed-out mutation committed. --- # Retry safety | Outcome | Caller action | | --- | --- | | Network timeout on read | Retry with backoff. | | Timeout on idempotent mutation | Retry identical payload with the same idempotency key. | | 400/413/415/422 | Change the request; do not retry unchanged. | | 401/403 | Replace credentials or authorization; do not probe. | | 404 | Verify ID/type; wrong-owner and unknown are indistinguishable. | | 409 state conflict | Reread and choose an allowed action. | | 409 idempotency conflict | Use original payload or a new key for genuinely new intent. | | 412 | Reread, reconcile, and use the new ETag. | | 429/503 | Honor `Retry-After` and add jitter. | | 500 | Retry reads or idempotent writes cautiously; retain request ID. | --- # Running workflows with Node.js Use Node.js 22 or newer and a disposable workspace. Set `STEADYDO_API_KEY` without printing it; the [quickstart](/api/docs/quickstart) shows a masked prompt. Copy this helper and the JavaScript block from a workflow into one `.mjs` file. Blocks name their prerequisites. For the complete sequence, run bootstrap workspace, manage standalone work, work Next Up, recover conflicts, create custom process, maintain template, create/operate order, divide/duplicate, archive/recover, then rotate key. Finish with the cleanup block below to permanently remove the fictional resources through inspected previews. The complete sequence needs all six scopes because it includes configuration, archive, previewed deletion and disposable-key management. Narrow a key to the listed scopes for a single workflow. Keep returned IDs in this local script context; do not guess them. Unexpected statuses stop execution and mutations are never retried automatically. ```javascript const base = (process.env.STEADYDO_BASE || 'https://dashboard.steadydo.com/api/v1').replace(/\/$/, ''); const key = process.env.STEADYDO_API_KEY; if (!key) throw new Error('Set STEADYDO_API_KEY'); const run = `docs-${crypto.randomUUID()}`; let sequence = 0; function check(condition, message) { if (!condition) throw new Error(message); } async function request(method, route, body, etag, expected = 200, credential = key, identity) { const headers = { Authorization: `Bearer ${credential}`, Accept: 'application/json' }; if (body !== undefined) { headers['Content-Type'] = 'application/json'; headers['Idempotency-Key'] = identity || `${run}-${++sequence}`; } if (etag) headers['If-Match'] = etag; const response = await fetch(`${base}${route}`, { method, headers, redirect: 'error', ...(body === undefined ? {} : { body: JSON.stringify(body) }) }); const value = await response.json(); check(response.status === expected, `${method} ${route}: HTTP ${response.status}; ${value.code || 'unexpected response'}; request ${value.requestId || value.meta?.requestId || 'unknown'}`); return { data: value.data, problem: value, etag: response.headers.get('etag'), headers: response.headers }; } const get = route => request('GET', route); async function action(route, body = {}, method = 'POST', readRoute = route.replace(/\/[^/]+$/, ''), expected = 200) { const current = await get(readRoute); check(current.etag, `No ETag for ${readRoute}`); return request(method, route, body, current.etag, expected); } async function allPages(route) { const rows = []; let cursor; do { const page = await get(`${route}${route.includes('?') ? '&' : '?'}limit=25${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`); rows.push(...page.data.items); cursor = page.data.nextCursor; } while (cursor); return rows; } ``` Each guide asserts expected state. On failure inspect the request ID and persisted resources; do not rerun the entire sequence with new keys indiscriminately. An uncertain write must retain its original identity as described in [idempotency](/api/docs/idempotency). Local HTTP execution is not deployed-environment certification. ## Clean up the combined fictional workflow Run this final block only after all Node workflow examples completed in the same script. It permanently deletes the three exact example orders, the custom process and its five example items, and the example company. The task example already deleted its task and the rotation example revoked its keys. Inspect every preview before adapting this to real data. Base system configuration created by bootstrap is retained because it is required workspace configuration; it is not a disposable custom process. Audit history and revoked-key records follow the documented retention policy. ```javascript async function deleteExample(route) { let current = await get(route); if (!current.data.archived) await request('POST', `${route}/archive`, {}, current.etag); current = await get(route); const preview = await request('POST', `${route}/delete-preview`, {}, current.etag); check(preview.data.effects && preview.data.confirmationToken, 'Cleanup preview missing'); await request('POST', `${route}/permanent-delete`, {confirmationToken:preview.data.confirmationToken}, current.etag); await request('GET', route, undefined, undefined, 404); } for (const id of [order.id, duplicate.id, divided.data.created.id]) await deleteExample(`/orders/${id}`); const cleanupProcessRoute = `/processes/${processId}`; let cleanupProcess = await get(cleanupProcessRoute); const archiveProcessPreview = await request('POST', `${cleanupProcessRoute}/archive-preview`, {}, cleanupProcess.etag); check(archiveProcessPreview.data.effects, 'Process archive effects missing'); await request('POST', `${cleanupProcessRoute}/archive`, {confirmationToken:archiveProcessPreview.data.confirmationToken}, cleanupProcess.etag); cleanupProcess = await get(cleanupProcessRoute); const configurationPreview = await request('POST', `${cleanupProcessRoute}/delete-preview`, {}, cleanupProcess.etag); await request('POST', `${cleanupProcessRoute}/permanent-delete`, {confirmationToken:configurationPreview.data.confirmationToken}, cleanupProcess.etag); // Configuration deletion deliberately retains an immutable snapshot and item history. // Rediscover that snapshot's version through the public export before final deletion. const retainedSnapshot = (await allPages('/workspace/export')).find(row => row.type === 'deletedProcessSnapshots' && row.value.sourceProcessId === processId); check(retainedSnapshot, 'Retained process snapshot missing'); const snapshotETag = `"v${retainedSnapshot.value.version}"`; const finalProcessPreview = await request('POST', `${cleanupProcessRoute}/delete-preview`, {}, snapshotETag); check(finalProcessPreview.data.effects, 'Final process effects missing'); await request('POST', `${cleanupProcessRoute}/permanent-delete`, {confirmationToken:finalProcessPreview.data.confirmationToken}, snapshotETag); await request('GET', `${cleanupProcessRoute}/items`, undefined, undefined, 404); await deleteExample(`/companies/${company.id}`); ``` --- # Running the workflow examples Use Bash, curl, and jq. Obtain a key through signed-in API access and give it only the scopes required by the operations you will run. The workflow examples create fictional data in your workspace; run them in a disposable test account. A successful schema check is not proof that an example has been executed against your deployment. Load these helpers once in your shell. They stop on HTTP errors, capture the exact ETag from a read, and print only response bodies. They do not automatically retry writes: if a response is lost, resend it with the original explicit idempotency key and identical body. ```bash set -euo pipefail read -rsp 'SteadyDo API key: ' STEADYDO_API_KEY; echo export BASE='https://dashboard.steadydo.com/api/v1' RUN="docs-$(date +%s)-$RANDOM" api_get() { curl --fail-with-body -sS "$BASE$1" \ -H "Authorization: Bearer $STEADYDO_API_KEY" } api_write() { local method="$1" route="$2" key="$3" body="$4" etag="${5:-}" local args=() if test -n "$etag"; then args+=(-H "If-Match: $etag"); fi curl --fail-with-body -sS -X "$method" "$BASE$route" \ -H "Authorization: Bearer $STEADYDO_API_KEY" \ -H "Idempotency-Key: $key" -H 'Content-Type: application/json' \ "${args[@]}" --data "$body" } api_etag() { local headers headers=$(mktemp) curl --fail-with-body -sS -D "$headers" "$BASE$1" \ -H "Authorization: Bearer $STEADYDO_API_KEY" >/dev/null awk 'tolower($1)=="etag:" {gsub("\r", "", $2); print $2}' "$headers" rm "$headers" } ``` `api_write POST /tasks "$RUN-create" '{"title":"Book photographer"}'` is a complete curl request through this helper. Subsequent snippets use returned IDs, never invented IDs. Assign an ETag immediately before a versioned action. An ETag is opaque; preserve its quotes and never synthesize it from an integer. Keep each `RUN` value and body if a request needs recovery. The [operation index](/api/operation-index.md) links every operation to a workflow. [OpenAPI](/api/openapi.json) includes a standalone curl snippet and schema-validated request/success/problem examples for every operation. On `412`, stop, reread, reconcile, and use a new idempotency key; on `429`, honor Retry-After. Never continue a multistep workflow after an unsuccessful step. End the session with `unset STEADYDO_API_KEY`. --- # Workflow states and schedules Read the parent order or process item and select the nested stage/task by its returned ID. Nested task mutations use task.version; stage mutations use stage.version; task creation/reordering uses the containing stage version; stage creation uses the parent version. IDs must match every path parent. See [concurrency](/api/docs/concurrency). An open task can be completed, pinned/unpinned, edited or explicitly skipped. Complete/reopen and skip/unskip are distinct transitions: completion records work done, while skip marks it not required. Reopen restores work from completion; unskip restores required work. Completed/skipped values may be represented by the resource-specific status enum (do not assume all public DTOs use identical words). Parent lifecycle restrictions still apply: archived work is read-only and must be restored before workflow changes. Stage completion requires its required tasks to be satisfied; do not force a stage complete merely because one task is done. Reopening/skipping stages can recompute which stage is active and dependent schedules. Invalid transitions return409 state_conflict with no partial edit; reread allowedActions/current state instead of blindly retrying. See [409 recovery](/api/docs/errors#409--state-and-idempotency-conflicts). Add/edit/delete/reorder task operations change only the copied instance workflow. A task can move one position up/down or, for moveOrderTask, into an owned destinationStageId at zero-based position. Do not combine direction with destination fields. A template key is provenance, not permission to edit the shared template. Schedules use each endpoint's exact enum: none, manual, stage-deadline, shipment-relative and weekly modes have different effects. Read [dates](/api/docs/dates) for calendar/working-day rules. Manual due dates and clearing use the schema's nullable fields. Shipment changes recompute shipment-relative tasks; stage activation changes inherited deadlines. Process-item recurrence accepts only stopOffsetDays and rejects conflicting schedule options. Progress and Next Up/Attention are derived, read-only projections. After an action, reread the parent and queues; the response is not a guarantee that another writer has not since changed them. History contains semantic events with actor/request attribution; idempotent replay does not create a second event. See [activity](/api/docs/activity). --- # Workflow: archive and recover work ## Executable Node.js alternative Run [manage standalone work](/api/docs/workflows/manage-standalone-work) first with the Node helper. This permanently removes only the fictional `task` created there; do not substitute an unrelated ID. The preview is an explicit decision point for real data. ```javascript const beforeArchive = (await get(`/tasks/${task.id}`)).data; await action(`/tasks/${task.id}/archive`); check((await allPages('/archive')).some(row => row.id === task.id), 'Archived task undiscoverable'); await action(`/tasks/${task.id}/restore`); const afterRestore = (await get(`/tasks/${task.id}`)).data; check(!afterRestore.archived && afterRestore.notes === beforeArchive.notes, 'Restore lost task content'); await action(`/tasks/${task.id}/archive`); const deleteCurrent = await get(`/tasks/${task.id}`); const deletePreview = await request('POST', `/tasks/${task.id}/delete-preview`, {}, deleteCurrent.etag); check(deletePreview.data.confirmationToken && deletePreview.data.effects, 'Deletion preview missing'); await request('POST', `/tasks/${task.id}/permanent-delete`, {confirmationToken:deletePreview.data.confirmationToken}, deleteCurrent.etag); await request('GET', `/tasks/${task.id}`, undefined, undefined, 404); ``` Create a fictional task with the [workflow shell](/api/docs/workflow-shell) or [quickstart](/api/docs/quickstart). Use its returned `TASK_ID`: ```bash api_write POST "/tasks/$TASK_ID/archive" "$RUN-archive" '{}' "$(api_etag "/tasks/$TASK_ID")" api_get /archive api_write POST "/tasks/$TASK_ID/restore" "$RUN-restore" '{}' "$(api_etag "/tasks/$TASK_ID")" api_get "/tasks/$TASK_ID" ``` Expected state: the task is restored with previous content. `listArchive` discovers archived resources; archive/restore require `archive:write`. Parent-process archive can make children read-only; restore the parent before editing children. Only if permanent deletion of this fictional task is intended, archive it again and inspect a preview. Deletion additionally requires `destructive:write` and is not part of the reversible quickstart: ```bash api_write POST "/tasks/$TASK_ID/archive" "$RUN-archive-delete" '{}' "$(api_etag "/tasks/$TASK_ID")" ETAG=$(api_etag "/tasks/$TASK_ID") PREVIEW=$(api_write POST "/tasks/$TASK_ID/delete-preview" "$RUN-delete-preview" '{}' "$ETAG") jq '.data.effects' <<< "$PREVIEW" CONFIRM=$(jq -ec '{confirmationToken:.data.confirmationToken}' <<< "$PREVIEW") # Run only after inspecting effects and deciding to permanently delete: api_write POST "/tasks/$TASK_ID/permanent-delete" "$RUN-delete" "$CONFIRM" "$ETAG" ``` Preview returns `200` and a token bound to caller, operation, resource version, and effects. Confirmation returns the deleted result; a later GET returns `404`. Expired or stale previews require rereading and a new preview. Recover a lost deletion response with the same key and body. For processes and other broad resources, call the resource's archive/deletion preview first. Present effects and cascades. Use the returned ten-minute single-use confirmation token only with the same actor, API key, operation, resource, aggregate version, and scope. Call archive, inspect `listArchive`, and use the typed restore link. Expected state: archived work leaves active views but retains history and snapshots. Permanent deletion occurs only after explicit confirmation. --- # Workflow: bootstrap a workspace ## Executable Node.js alternative Load the [Node helper](/api/docs/workflow-node). This initializes only absent base configuration, reads bounded collections, creates one fictional company, and restores the original timezone after checking the preference write. ```javascript const capabilities = (await get('/capabilities')).data; check(['workspace:read','workspace:write','config:write'].every(scope => capabilities.heldScopes.includes(scope)), 'Missing bootstrap scope'); await get('/workspace/summary'); const originalPreferences = (await get('/preferences')).data; await request('PATCH', '/preferences', {timeZone:'Europe/Moscow'}); check((await get('/preferences')).data.timeZone === 'Europe/Moscow', 'Timezone was not saved'); await request('PATCH', '/preferences', {timeZone:originalPreferences.timeZone}); await request('POST', '/processes/bootstrap', {}); const processes = await allPages('/processes'); check(processes.some(process => process.slug === 'orders'), 'Orders configuration missing'); const company = (await request('POST', '/companies', {name:`Northwind ${run}`,code:'NW-DOCS'}, undefined, 201)).data; check((await get(`/companies/${company.id}`)).data.name === company.name, 'Company not persisted'); await allPages('/companies'); await allPages('/tasks'); ``` Load the [workflow shell](/api/docs/workflow-shell). Use a key with `workspace:read`, `workspace:write`, and `config:write`. Inspect capabilities before configuration; a missing operation or scope is a reason to stop. ```bash api_get /capabilities | jq '.data | {apiVersion,heldScopes,limits}' api_get /workspace/summary api_get /preferences api_write PATCH /preferences "$RUN-timezone" '{"timeZone":"Europe/Moscow"}' COMPANY=$(api_write POST /companies "$RUN-company" '{"name":"Northwind Studio","code":"NW-DOCS"}') COMPANY_ID=$(jq -er '.data.id' <<< "$COMPANY") api_get "/companies/$COMPANY_ID" ``` Expected responses: reads return `200`; `createCompany` returns `201` with `data.id`; `updatePreferences` returns `200` and `data.timeZone: Europe/Moscow`. Expected final state: one fictional company exists and workspace local-date calculations use the saved timezone. UTC timestamps remain unchanged. Inspect existing preferences first and restore the original timezone afterward if this was only a test. Do not repeatedly bootstrap an existing workspace or assume a base configuration is absent solely because a list page is empty. Call `getCapabilities`, then `getWorkspaceSummary`. If base Orders configuration is absent, call `ensureBaseConfiguration` with idempotency and reread processes. Inspect `listProcesses`, `listCompanies`, and `listTasks`; do not request a raw database dump. Expected state: Orders is available with its supported configuration, no existing tracked work was modified, and the agent knows limits, scopes, and allowed actions. --- # Workflow: create a custom process ## Executable Node.js alternative Load the [Node helper](/api/docs/workflow-node). This fuller alternative creates a required field, saved view settings, a populated template and two independently copied items. Keep these IDs for template maintenance. ```javascript const createdProcess = (await request('POST', '/processes', {name:`Product Development ${run}`,color:'#35648A'}, undefined, 201)).data; const processId = createdProcess.process.id; await action(`/processes/${processId}/fields`, {field:{key:'budget',type:'currency',labelEn:'Budget',labelRu:'Бюджет',required:true}}, 'POST', `/processes/${processId}`, 201); await action(`/processes/${processId}/views`, {pageSettings:{table:{columns:['title','budget']}}}, 'PUT', `/processes/${processId}`); const initialStructure = {stages:[{key:'prepare',title:{en:'Prepare',ru:'Подготовка'},position:0,tasks:[{key:'brief',title:{en:'Write brief',ru:'Подготовить описание'},position:0}]}]}; const template = (await action(`/processes/${processId}/templates`, {nameEn:'Standard launch',structure:initialStructure}, 'POST', `/processes/${processId}`, 201)).data; const templateRoute = `/processes/${processId}/templates/${template.id}`; await action(`${templateRoute}/publish`); await action(`${templateRoute}/set-default`); const item = (await action(`/processes/${processId}/items`, {title:'Ceramic lamp',templateId:template.id,fieldValues:{budget:125.5}}, 'POST', `/processes/${processId}`, 201)).data; const untouchedItem = (await action(`/processes/${processId}/items`, {title:'Untouched lamp',templateId:template.id,fieldValues:{budget:200}}, 'POST', `/processes/${processId}`, 201)).data; const itemRoute = `/processes/${processId}/items/${item.id}`; const untouchedRoute = `/processes/${processId}/items/${untouchedItem.id}`; const itemBeforeTemplateEdit = (await get(itemRoute)).data; check(itemBeforeTemplateEdit.fieldValues.budget === 125.5 && itemBeforeTemplateEdit.stages.length === 1, 'Required field or workflow copy missing'); ``` Call `createProcess`, add fields with `createProcessField`, configure views with `updateProcessViews`, create a draft with `createProcessTemplate`, define stages/checklist tasks, then `publishProcessTemplate` and `setDefaultProcessTemplate`. Create the first item with `createProcessItem`. Expected state: the process automatically has its standard table, dashboard, detail, workflow, and history structure; the item owns a template snapshot. Use the [workflow shell](/api/docs/workflow-shell) with `workspace:read`, `workspace:write`, and `config:write`. This sequence creates one process, one published template, and an item; replace names before running it in a real workspace. ```bash PROCESS=$(api_write POST /processes "$RUN-process" '{"name":"Product Development","color":"#35648A"}') PROCESS_ID=$(jq -er '.data.process.id' <<< "$PROCESS") TEMPLATE=$(api_write POST "/processes/$PROCESS_ID/templates" "$RUN-template" \ '{"nameEn":"Standard launch","structure":{"stages":[{"key":"prepare","title":{"en":"Prepare","ru":"Подготовка"},"position":0,"tasks":[{"key":"brief","title":{"en":"Write brief","ru":"Подготовить описание"},"position":0}]}]}}' \ "$(api_etag "/processes/$PROCESS_ID")") TEMPLATE_ID=$(jq -er '.data.id' <<< "$TEMPLATE") api_write POST "/processes/$PROCESS_ID/templates/$TEMPLATE_ID/publish" "$RUN-publish" '{}' \ "$(api_etag "/processes/$PROCESS_ID/templates/$TEMPLATE_ID")" api_write POST "/processes/$PROCESS_ID/templates/$TEMPLATE_ID/set-default" "$RUN-default" '{}' \ "$(api_etag "/processes/$PROCESS_ID/templates/$TEMPLATE_ID")" ITEM_BODY=$(jq -n --arg t "$TEMPLATE_ID" '{title:"Ceramic lamp",templateId:$t}') ITEM=$(api_write POST "/processes/$PROCESS_ID/items" "$RUN-item" "$ITEM_BODY" \ "$(api_etag "/processes/$PROCESS_ID")") ITEM_ID=$(jq -er '.data.id' <<< "$ITEM") api_get "/processes/$PROCESS_ID/items/$ITEM_ID" | jq '.data | {id,title,templateId,version}' ``` Creation returns `201`: process creation returns `data.process.id` and `data.defaultTemplateId`; template and item creation return `data.id`. Publication and default selection return `200`. A template must contain a workflow before publication; this example includes one stage and one checklist task. After item creation, template edits do not silently rewrite it. Use [maintain-template](/api/docs/workflows/maintain-template) for explicit scoped updates. Field creation and reordering use the process ETag; updating or hiding a field uses that field's version from the process representation, formatted as `"vN"`. --- # Workflow: create and operate an order ## Executable Node.js alternative Load the [Node helper](/api/docs/workflow-node). This reads copied workflow IDs and uses the nested target's version for its actions; the task is restored after a complete/reopen cycle. ```javascript const order = (await request('POST', '/orders', {specNo:'SPEC-DOCS-104',supplier:'Northwind Studio',invoiceNumber:'INV-104',orderNumber:'ORD-104',containerQty:1,plannedShipmentDate:'2026-10-01',comment:'Documentation example'}, undefined, 201)).data; const orderRoute = `/orders/${order.id}`; await action(orderRoute, {comment:'Delivery confirmed by fictional supplier'}, 'PATCH', orderRoute); const orderCurrent = (await get(orderRoute)).data; const orderStage = orderCurrent.stages[0]; const orderTask = orderCurrent.tasks.find(candidate => candidate.stageId === orderStage.id); check(orderTask, 'Order workflow task missing'); const orderTaskRoute = `${orderRoute}/stages/${orderStage.id}/tasks/${orderTask.id}`; await request('POST', `${orderTaskRoute}/complete`, {}, `"v${orderTask.version}"`); const completedOrderTask = (await get(orderRoute)).data.tasks.find(candidate => candidate.id === orderTask.id); check(completedOrderTask.status === 'done', 'Nested task not completed'); await request('POST', `${orderTaskRoute}/reopen`, {}, `"v${completedOrderTask.version}"`); check((await allPages(`${orderRoute}/activity`)).length >= 3, 'Order history missing'); ``` Load the [workflow shell](/api/docs/workflow-shell) with `workspace:read` and `workspace:write`. An order may be created without a company; add an owned `companyId` to use company context. This example deliberately creates fictional work. ```bash ORDER=$(api_write POST /orders "$RUN-order" \ '{"specNo":"SPEC-DOCS-104","supplier":"Northwind Studio","invoiceNumber":"INV-104","orderNumber":"ORD-104","containerQty":1,"plannedShipmentDate":"2026-10-01","comment":"Documentation example"}') ORDER_ID=$(jq -er '.data.id' <<< "$ORDER") api_get "/orders/$ORDER_ID" | jq '.data | {id,specNo,stages,tasks,version}' api_write PATCH "/orders/$ORDER_ID" "$RUN-order-comment" \ '{"comment":"Delivery confirmed by fictional supplier"}' "$(api_etag "/orders/$ORDER_ID")" api_get "/orders/$ORDER_ID/activity" ``` `createOrder` returns `201`; `updateOrder` returns `200`. Expected state: one active order with the chosen shipment date and updated comment. The activity read returns resource history with safe actor attribution. Use stage/task IDs from the order response for workflow actions; use the nested task version for task actions and the stage version for stage actions, formatted as a strong ETag such as "v3". Creating a task uses its stage version; creating a stage uses the order version. Completing, skipping, undoing, or changing schedule inputs can recompute stage progress and dependent dates. Company defaults and source template edits affect future orders; an existing order uses its own workflow copy. `saveOrderAsTemplate` snapshots that structure into a new published template and requires `config:write`. Read `listCompanies` and `listProcessTemplates` for Orders. `createOrder` resolves an explicit template first, otherwise the selected company's future-order default, then the built-in workflow. Record the order ETag. Use `updateOrder`, order stage/task actions, and `completeOrder` with current ETags and idempotency. Verify derived dates and history with `listOrderActivity`. Expected state: one order owns a workflow snapshot; later company/template edits do not silently change it. --- # Workflow: divide or duplicate work safely ## Executable Node.js alternative First run the Node alternative in [create and operate an order](/api/docs/workflows/create-operate-order). This intentionally copies the fictional order, applies its displayed division and replays the same request to prove no second copy is created. Review effects before applying to real work. ```javascript const duplicate = (await action(`${orderRoute}/duplicate`, {}, 'POST', orderRoute, 201)).data; check(duplicate.id !== order.id, 'Duplicate reused source ID'); const beforeDivide = await get(orderRoute); const divisionPreview = await request('POST', `${orderRoute}/divide-preview`, {}, beforeDivide.etag); check(divisionPreview.data.confirmationToken && divisionPreview.data.effects, 'Division preview missing'); const divisionBody = {confirmationToken:divisionPreview.data.confirmationToken}; const divisionIdentity = `${run}-order-divide`; const divided = await request('POST', `${orderRoute}/divide`, divisionBody, beforeDivide.etag, 200, key, divisionIdentity); const dividedReplay = await request('POST', `${orderRoute}/divide`, divisionBody, beforeDivide.etag, 200, key, divisionIdentity); check(JSON.stringify(dividedReplay.data) === JSON.stringify(divided.data), 'Division replay changed outcome'); check(dividedReplay.headers.get('idempotency-replayed') === 'true', 'Division replay not recognized'); check((await get(orderRoute)).data.containerQty === beforeDivide.data.containerQty, 'Division unexpectedly allocated source quantity'); // If the custom-process guide also ran, verify the different item division semantics. if (typeof itemRoute !== 'undefined') { const itemDuplicate = (await action(`${itemRoute}/duplicate`, {}, 'POST', itemRoute, 201)).data; check(itemDuplicate.id !== item.id && !(await get(itemRoute)).data.archived, 'Item duplication altered source'); const divideItemCurrent = await get(itemRoute); const divideItemPreview = await request('POST', `${itemRoute}/divide-preview`, {titles:['Ceramic lamp - A','Ceramic lamp - B']}, divideItemCurrent.etag); const itemDivisionIdentity = `${run}-item-divide`; const itemDivisionBody = {confirmationToken:divideItemPreview.data.confirmationToken}; const itemDivision = await request('POST', `${itemRoute}/divide`, itemDivisionBody, divideItemCurrent.etag, 200, key, itemDivisionIdentity); const itemDivisionReplay = await request('POST', `${itemRoute}/divide`, itemDivisionBody, divideItemCurrent.etag, 200, key, itemDivisionIdentity); check(JSON.stringify(itemDivisionReplay.data) === JSON.stringify(itemDivision.data), 'Item division replay changed outcome'); check((await get(itemRoute)).data.archived, 'Item division did not archive source'); } ``` Load the [workflow shell](/api/docs/workflow-shell) and create a fictional order with [create-operate-order](/api/docs/workflows/create-operate-order). Duplication preserves the source and creates an independent order. Division has different effects; inspect its preview before deciding. ```bash COPY=$(api_write POST "/orders/$ORDER_ID/duplicate" "$RUN-duplicate" '{}' "$(api_etag "/orders/$ORDER_ID")") COPY_ID=$(jq -er '.data.id' <<< "$COPY") api_get "/orders/$ORDER_ID" api_get "/orders/$COPY_ID" ETAG=$(api_etag "/orders/$ORDER_ID") PREVIEW=$(api_write POST "/orders/$ORDER_ID/divide-preview" "$RUN-divide-preview" '{}' "$ETAG") jq '.data.effects' <<< "$PREVIEW" ``` Expected state here: original plus one duplicate; preview has not divided anything. To intentionally apply the displayed division, submit the token and original ETag: ```bash CONFIRM=$(jq -ec '{confirmationToken:.data.confirmationToken}' <<< "$PREVIEW") api_write POST "/orders/$ORDER_ID/divide" "$RUN-divide" "$CONFIRM" "$ETAG" api_get "/orders/$ORDER_ID/activity" ``` Record returned IDs; do not guess them. Process-item division uses `previewProcessItemDivision` with explicit titles, then `divideProcessItem`. Retry the identical key/body after lost responses to avoid duplicate copies. No shared source-template edit is performed by duplication or division. Order division creates a linked copy of the complete source order; it does not partition or conserve quantities across the source and copy. Process-item division creates one workflow copy for each requested title and archives the source item. These effects differ from duplication and must be inspected in the preview. If your intent is to allocate quantities, make explicit, validated edits to the resulting orders after creation; do not assume division did that allocation. Expected state: returned IDs are distinct, the previewed copy/archive effects are present, and replaying the identical request creates no extra records. Verify each result's content and history; there is no automatic quantity-conservation guarantee. --- # Workflow: maintain a template safely ## Executable Node.js alternative First run the Node alternative in [create custom process](/api/docs/workflows/create-custom-process). Inspect the preview before applying it outside this disposable example. The assertions compare both selected and untouched items. ```javascript const untouchedBefore = (await get(untouchedRoute)).data; const expandedStructure = {stages:[...initialStructure.stages,{key:'review',title:{en:'Review',ru:'Проверка'},position:1,tasks:[{key:'final-review',title:{en:'Final review',ru:'Итоговая проверка'},position:0}]}]}; await action(templateRoute, {structure:expandedStructure}, 'PATCH', templateRoute); check(JSON.stringify((await get(itemRoute)).data) === JSON.stringify(itemBeforeTemplateEdit), 'Template edit silently changed existing item'); const templateCurrent = await get(templateRoute); const templatePreview = await request('POST', `${templateRoute}/apply-preview`, {itemIds:[item.id]}, templateCurrent.etag); check(templatePreview.data.confirmationToken && templatePreview.data.effects, 'Preview missing effects or token'); await request('POST', `${templateRoute}/apply`, {confirmationToken:templatePreview.data.confirmationToken}, templateCurrent.etag); check((await get(itemRoute)).data.stages.length === 2, 'Selected item did not receive missing stage'); check(JSON.stringify((await get(untouchedRoute)).data) === JSON.stringify(untouchedBefore), 'Untouched item changed'); ``` Start with the process, template, and item IDs created by [create-custom-process](/api/docs/workflows/create-custom-process), and load the [workflow shell](/api/docs/workflow-shell). Use `workspace:read`, `workspace:write`, and `config:write`. A template is configuration for future items; editing it is not permission to change every existing item. ```bash api_get "/processes/$PROCESS_ID/templates/$TEMPLATE_ID" api_get "/processes/$PROCESS_ID/items/$ITEM_ID" > item-before.json api_write PATCH "/processes/$PROCESS_ID/templates/$TEMPLATE_ID" "$RUN-template-edit" \ '{"structure":{"stages":[{"key":"prepare","title":{"en":"Prepare","ru":"Подготовка"},"position":0,"tasks":[{"key":"brief","title":{"en":"Write brief","ru":"Подготовить описание"},"position":0}]},{"key":"review","title":{"en":"Review","ru":"Проверка"},"position":1,"tasks":[{"key":"final-review","title":{"en":"Final review","ru":"Итоговая проверка"},"position":0}]}]}}' \ "$(api_etag "/processes/$PROCESS_ID/templates/$TEMPLATE_ID")" ETAG=$(api_etag "/processes/$PROCESS_ID/templates/$TEMPLATE_ID") BODY=$(jq -n --arg id "$ITEM_ID" '{itemIds:[$id]}') PREVIEW=$(api_write POST "/processes/$PROCESS_ID/templates/$TEMPLATE_ID/apply-preview" \ "$RUN-template-preview" "$BODY" "$ETAG") jq '.data.effects' <<< "$PREVIEW" # Inspect all displayed effects and conflicts before applying this explicit scope. CONFIRM=$(jq -ec '{confirmationToken:.data.confirmationToken}' <<< "$PREVIEW") api_write POST "/processes/$PROCESS_ID/templates/$TEMPLATE_ID/apply" \ "$RUN-template-apply" "$CONFIRM" "$ETAG" api_get "/processes/$PROCESS_ID/items/$ITEM_ID" > item-after.json ``` This example retains the preparation stage from the preceding guide and adds one review stage/task. Template application is additive: missing template stages/tasks are copied into selected items while existing workflow structure and progress are preserved. Arbitrary change instructions are not supported; the optional legacy `changes` field accepts only `[{"kind":"add-missing"}]`. A validation or state conflict is a stop condition. Preview returns `200` with actual additions and a short-lived token bound to selected item versions. Apply returns affected IDs and counts. Compare an untouched item before/after when testing. Execution against an isolated deployment remains part of release certification. Read the published template and ETag, update its structure, then `previewTemplateApplication` against an explicit item scope. Inspect additions and affected versions before calling `applyTemplateToItems` with the confirmation token, template ETag, and idempotency key. Reread selected and untouched items. Expected state: selected items receive only approved changes; untouched existing items remain byte-for-byte equivalent in workflow meaning. ## Template field and display defaults Process field definitions remain process-level configuration. Templates may set `fieldDefaults`, a typed map keyed by those field keys, and `displaySettings` containing `primaryTitleField`, `titleFields`, and `tableColumns`. Display references must identify a process field (key or ID), or the built-in `title`, `company`, `status`, `createdAt`, or `updatedAt` columns. Unknown settings and cross-owner company references are rejected. Creating an item or order merges template defaults with caller field values; caller values win and required/type checks run on the result. Display settings are copied into `templateDisplaySettings` on the instance. Editing or duplicating the template never modifies existing instances. An explicit selected-item apply adds missing default values while preserving existing/customized values and copies the selected template display settings to those selected instances only. Renaming a process field moves template defaults and key-based display references to the new key. Explicit field deletion removes its template defaults/display references as part of the same operation. Existing instance display snapshots remain historical copies; deleting field data still removes the corresponding current instance values under the documented destructive flow. --- # Workflow: manage standalone work ## Executable Node.js alternative Load the [Node helper](/api/docs/workflow-node). Keep `task` for the Next Up, conflict and archive examples. This deliberately returns the restored task to active state for those later examples. ```javascript const task = (await request('POST', '/tasks', {title:'Book photographer',dueDate:'2026-10-01',notes:'Fictional documentation example',...(typeof company === 'undefined' ? {} : {companyId:company.id}),checklistItems:[{text:'Confirm availability'}]}, undefined, 201)).data; const checklistRoute = `/tasks/${task.id}/checklist-items/${task.checklistItems[0].id}`; await action(checklistRoute, {text:'Confirm availability and fee'}, 'PATCH', `/tasks/${task.id}`); await action(`${checklistRoute}/complete`, {}, 'POST', `/tasks/${task.id}`); check((await get(`/tasks/${task.id}`)).data.checklistItems[0].completed, 'Checklist completion missing'); await action(`${checklistRoute}/reopen`, {}, 'POST', `/tasks/${task.id}`); const extraChecklist = (await action(`/tasks/${task.id}/checklist-items`, {text:'Temporary reminder'}, 'POST', `/tasks/${task.id}`, 201)).data; const extraChecklistId = extraChecklist.checklistItems.find(row => row.text === 'Temporary reminder').id; await action(`/tasks/${task.id}/checklist-items/${extraChecklistId}`, {}, 'DELETE', `/tasks/${task.id}`); await action(`/tasks/${task.id}`, {dueDate:'2026-10-02'}, 'PATCH', `/tasks/${task.id}`); await action(`/tasks/${task.id}/complete`); await action(`/tasks/${task.id}/archive`); await action(`/tasks/${task.id}/restore`); const restoredTask = (await get(`/tasks/${task.id}`)).data; check(restoredTask.status === 'completed' && !restoredTask.archived, 'Restore lost completed state'); check(restoredTask.checklistItems.length === 1 && restoredTask.dueDate === '2026-10-02', 'Restore lost checklist or schedule'); await action(`/tasks/${task.id}/reopen`); await action(`/tasks/${task.id}/pin`); check((await get(`/tasks/${task.id}`)).data.pinned, 'Pin not saved'); ``` Follow the [quickstart](/api/docs/quickstart) to create, complete, and archive a task. For a checklist and schedule, load the [workflow shell](/api/docs/workflow-shell) with `workspace:read`, `workspace:write`, and `archive:write`: ```bash TASK=$(api_write POST /tasks "$RUN-task" \ '{"title":"Book photographer","dueDate":"2026-10-01","notes":"Fictional documentation example","checklistItems":[{"text":"Confirm availability"}]}') TASK_ID=$(jq -er '.data.id' <<< "$TASK") api_get "/tasks/$TASK_ID" | jq '.data | {id,title,checklistItems,version}' api_write PATCH "/tasks/$TASK_ID" "$RUN-reschedule" '{"dueDate":"2026-10-02"}' \ "$(api_etag "/tasks/$TASK_ID")" api_write POST "/tasks/$TASK_ID/complete" "$RUN-complete" '{}' "$(api_etag "/tasks/$TASK_ID")" api_write POST "/tasks/$TASK_ID/archive" "$RUN-archive" '{}' "$(api_etag "/tasks/$TASK_ID")" api_write POST "/tasks/$TASK_ID/restore" "$RUN-restore" '{}' "$(api_etag "/tasks/$TASK_ID")" ``` Creation returns `201`; edits and actions return `200` task envelopes. Expected final state: the task is restored, preserves its checklist and schedule, and remains completed. `reopenTask` makes it open again. Company association is optional and must refer to an owned company. Checklist item IDs come from `data.checklistItems`; use the checklist routes rather than replacing internal task fields. All examples are fictional. Use `createTask` with optional company, schedule, notes, and checklist. Manage checklist items through nested operations; use task actions for completion, reopen, pin, duplicate, archive, and restore. Preview permanent deletion when offered and require user intent. Expected state: the standalone task appears in Home/Next Up while active, remains recoverable while archived, and is absent only after confirmed permanent deletion. --- # Workflow: recover from conflicts and partial workflows ## Executable Node.js alternative Run [manage standalone work](/api/docs/workflows/manage-standalone-work) first with the Node helper. This checks the exact rejection status, preserved state, deliberate reconciliation, replay and changed-payload conflict. It never swallows an arbitrary failure as a successful conflict test. ```javascript const beforeConflict = await get(`/tasks/${task.id}`); const editIdentity = `${run}-first-edit`; const firstEdit = await request('PATCH', `/tasks/${task.id}`, {notes:'First edit'}, beforeConflict.etag, 200, key, editIdentity); const stale = await request('PATCH', `/tasks/${task.id}`, {notes:'Second edit'}, beforeConflict.etag, 412); check(stale.problem.code === 'version_conflict', 'Wrong stale-write error'); check((await get(`/tasks/${task.id}`)).data.notes === 'First edit', 'Stale request overwrote saved change'); const replay = await request('PATCH', `/tasks/${task.id}`, {notes:'First edit'}, beforeConflict.etag, 200, key, editIdentity); check(replay.data.version === firstEdit.data.version && replay.headers.get('idempotency-replayed') === 'true', 'Replay repeated mutation'); const mismatch = await request('PATCH', `/tasks/${task.id}`, {notes:'Changed payload'}, beforeConflict.etag, 409, key, editIdentity); check(mismatch.problem.code === 'idempotency_conflict', 'Wrong replay mismatch error'); await action(`/tasks/${task.id}`, {notes:'First edit; second edit reconciled'}, 'PATCH', `/tasks/${task.id}`); check((await get(`/tasks/${task.id}`)).data.notes === 'First edit; second edit reconciled', 'Reconciliation missing'); ``` Use a separate disposable task to exercise invalid lifecycle and destructive-confirmation recovery. `previewTaskDeletion` is valid only after `archiveTask`. A confirmation is single-use and bound to the actor, operation, resource, and exact resource version for ten minutes. ```javascript const recovery = await request('POST', '/tasks', {title:`${run} recovery`, checklistItems:[{text:'Verify recovery'}]}, undefined, 201); const activePreview = await request('POST', `/tasks/${recovery.data.id}/delete-preview`, {}, recovery.etag, 409); check(activePreview.problem.code === 'state_conflict', 'Active deletion preview did not reject invalid lifecycle'); check((await get(`/tasks/${recovery.data.id}`)).data.archived === false, 'Invalid-state request changed the task'); const archived = await request('POST', `/tasks/${recovery.data.id}/archive`, {}, recovery.etag, 200); const stalePreview = await request('POST', `/tasks/${recovery.data.id}/delete-preview`, {}, archived.etag, 200); check(stalePreview.data.effects[0].taskId === recovery.data.id && stalePreview.data.effects[0].checklistItems === 1, 'Preview effect summary is incomplete'); const restored = await request('POST', `/tasks/${recovery.data.id}/restore`, {}, archived.etag, 200); const rearchived = await request('POST', `/tasks/${recovery.data.id}/archive`, {}, restored.etag, 200); const staleApply = await request('POST', `/tasks/${recovery.data.id}/permanent-delete`, {confirmationToken:stalePreview.data.confirmationToken}, rearchived.etag, 422); check(staleApply.problem.code === 'confirmation_expired', 'Changed resource did not invalidate preview'); // The same 422 is returned when an otherwise unchanged token passes its ten-minute expiry. // In either case, reread, obtain a fresh preview, inspect effects again, and only then confirm. const freshPreview = await request('POST', `/tasks/${recovery.data.id}/delete-preview`, {}, rearchived.etag, 200); const deleted = await request('POST', `/tasks/${recovery.data.id}/permanent-delete`, {confirmationToken:freshPreview.data.confirmationToken}, rearchived.etag, 200); check(deleted.data.deleted === true && deleted.data.id === recovery.data.id, 'Confirmed deletion did not identify the task'); await request('GET', `/tasks/${recovery.data.id}`, undefined, undefined, 404); ``` For bounded retry branches, use the response status and published `Retry-After`; never infer success from a transport failure. ```javascript async function retryRecoverable(send, {maxAttempts=3, sleep=milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)), jitter=() => Math.floor(Math.random()*250)}={}) { for (let attempt=1; attempt<=maxAttempts; attempt++) { const response = await send(); // send closes over the exact method, body and Idempotency-Key. if (response.status !== 429 && response.status !== 503) return response; const retryAfter = Number(response.headers.get('retry-after')); check(Number.isInteger(retryAfter) && retryAfter >= 1, 'Retry-After is missing or invalid'); if (attempt === maxAttempts) return response; await sleep(retryAfter*1000+jitter()); } throw new Error('Unreachable retry state'); } ``` Use the [workflow shell](/api/docs/workflow-shell) and a fictional task ID. This intentionally demonstrates a stale-version rejection without losing another writer's edit: ```bash OLD_ETAG=$(api_etag "/tasks/$TASK_ID") api_write PATCH "/tasks/$TASK_ID" "$RUN-first-edit" '{"notes":"First edit"}' "$OLD_ETAG" # Expect HTTP 412; this stale edit must not overwrite the first edit. if api_write PATCH "/tasks/$TASK_ID" "$RUN-stale-edit" '{"notes":"Second edit"}' "$OLD_ETAG"; then echo 'Unexpected stale-write success'; exit 1 fi api_get "/tasks/$TASK_ID" # Reconcile deliberately, then use a fresh version and a new mutation key. api_write PATCH "/tasks/$TASK_ID" "$RUN-reconciled" '{"notes":"First edit; second edit reconciled"}' \ "$(api_etag "/tasks/$TASK_ID")" ``` Expected final state: both intentional changes are represented in the original task notes; the stale request was rejected; the disposable recovery task was deleted only after a fresh inspected preview. For a transport timeout or `503`, do not immediately choose a new key: replay the original body/key because execution may be unknown. For `409 idempotency_conflict`, restore the original payload or choose a new key only for a genuinely new action. For `422 confirmation_expired`, reread and obtain a fresh preview. For `429` or `503`, wait at least Retry-After, add jitter, and cap retries. Keep request IDs for support and never include the Authorization header in a report. Multistep workflows are not automatically transactions. If step three fails after steps one and two succeed, inspect those resources and continue from the first incomplete step. For supported groups of up to 25 resource state actions, use `previewBatchActions` then `applyBatchActions`; they commit all actions together or none. The preview binds versions, so apply does not require a separate If-Match header. On 412, reread the aggregate, reconcile, and retry with its strong ETag. On state conflict, follow `allowedActions`. On expired confirmation, request a fresh preview. On idempotency conflict, use the original payload or a new key only for new intent. On 429/503, honor `Retry-After`; after timeouts replay identical idempotent writes. Expected state: no lost update, duplicate record, broadened scope, or unconfirmed destructive effect occurs. --- # Workflow: rotate an agent key ## Executable Node.js alternative Load the [Node helper](/api/docs/workflow-node). This creates and rotates a disposable read-only key, never the bootstrap credential. Production rotation additionally requires updating your integration's secret store before ending overlap. The example verifies rejection of the predecessor and then revokes its disposable successor; it prints no secret. ```javascript let disposableKey = (await request('POST', '/api-keys', {label:`Rotation example ${run}`,scopes:['workspace:read']}, undefined, 201)).data; let successor; try { const rotationIdentity = `${run}-rotate-disposable`; successor = (await request('POST', `/api-keys/${disposableKey.key.id}/rotate`, {overlapSeconds:300}, undefined, 200, key, rotationIdentity)).data; const lostSecretReplay = (await request('POST', `/api-keys/${disposableKey.key.id}/rotate`, {overlapSeconds:300}, undefined, 200, key, rotationIdentity)).data; check(lostSecretReplay.secretUnavailable === true && !lostSecretReplay.secret, 'One-time secret leaked on replay'); await request('GET', '/capabilities', undefined, undefined, 200, successor.secret); await request('POST', `/api-keys/${disposableKey.key.id}/revoke`, {}); await request('GET', '/capabilities', undefined, undefined, 401, disposableKey.secret); } finally { if (successor) await request('POST', `/api-keys/${successor.key.id}/revoke`, {}); else await request('POST', `/api-keys/${disposableKey.key.id}/revoke`, {}); disposableKey = undefined; successor = undefined; } ``` Load the [workflow shell](/api/docs/workflow-shell) with a key that has `keys:manage` and `workspace:read`. Inspect safe metadata and choose the key ID to rotate. Never print the rotation response, since it contains a one-time secret. ```bash api_get /api-keys | jq '.data' read -rp 'Key ID to rotate: ' KEY_ID ROTATION=$(api_write POST "/api-keys/$KEY_ID/rotate" "$RUN-rotate" '{"overlapSeconds":300}') NEW_SECRET=$(jq -er '.data.secret' <<< "$ROTATION") curl --fail-with-body -sS "$BASE/capabilities" -H "Authorization: Bearer $NEW_SECRET" >/dev/null # Save NEW_SECRET in your secret store and update the integration during overlap. unset ROTATION NEW_SECRET ``` Expected first response: `200` with replacement key metadata and `secret`. A replay intentionally returns safe key metadata with `secretUnavailable: true`, not the secret again. If the first response was lost, recover by rotating or issuing a new replacement under an authorized key; do not keep retrying hoping to recover the secret. Rotation scopes must fit both predecessor and caller, and expiry cannot extend either boundary. After validating the replacement, revoke the old key with `revokeApiKey`; never revoke your only working key before storing and testing a replacement. The optional overlap is at most 3600 seconds, with a default of zero. List metadata with `listApiKeys`, then call `rotateApiKey` using idempotency. The successor preserves or narrows scopes and cannot expire later than the caller. Copy the one-time secret immediately, validate `getCapabilities` with it, update the secret store, then end overlap/revoke the old key. Expected state: the replacement works, the old key is rejected after revocation, and no reusable secret exists in logs, docs, or idempotency storage. --- # Workflow: work the Next Up queue ## Executable Node.js alternative Run [manage standalone work](/api/docs/workflows/manage-standalone-work) first with the Node helper. Select the exact task just created, follow its returned canonical action link and ETag, then verify both resource and queue. Do not pick an arbitrary user's task. ```javascript const nextRows = await allPages('/next-up'); const selected = nextRows.find(row => row.type === 'task' && row.id === task.id); check(selected?.actions?.complete, 'Created task absent from Next Up'); const complete = selected.actions.complete; check(complete.href.startsWith('/api/v1/'), 'Unexpected action route'); await request(complete.method, complete.href.slice('/api/v1'.length), {}, complete.ifMatch); check((await get(`/tasks/${task.id}`)).data.status === 'completed', 'Queue action did not complete task'); check(!(await allPages('/next-up')).some(row => row.type === 'task' && row.id === task.id), 'Completed task remained queued'); await action(`/tasks/${task.id}/reopen`); ``` Load the [workflow shell](/api/docs/workflow-shell), then inspect the typed projection: ```bash api_get /next-up | jq '.data' ``` Do not PATCH a queue row. Identify whether the selected action refers to a standalone task, process-item task, or order task. Use its canonical resource route and target version as explained in [concurrency](/api/docs/concurrency). For a standalone task selected from the response: ```bash read -rp 'Standalone task ID selected from Next Up: ' TASK_ID api_get "/tasks/$TASK_ID" api_write POST "/tasks/$TASK_ID/complete" "$RUN-nextup-complete" '{}' \ "$(api_etag "/tasks/$TASK_ID")" api_get /next-up ``` Expected final state: the selected task is completed and the refreshed queue no longer offers the same open action. Undo with `reopenTask`; pin with `pinTask`. For order tasks use `completeOrderTask`, `reopenOrderTask`, and `pinOrderTask` with the selected nested task version from the order representation, formatted as "vN". For process-item tasks use matching `ProcessItemTask` operations with the selected nested task version from the item representation, formatted as "vN". Never infer a route from a display label. Call `getNextUp`. Each entry provides a canonical parent and allowed action link. Read that resource, then use its task/order/process-item completion, undo, pin, or unpin operation. Never PATCH the queue. Refresh `getNextUp` and optionally `getHome`. Expected state: canonical work and history changed once, and the projection reflects the new priority. --- # Public API terminology | Term | Meaning | | --- | --- | | process | A repeatable routine that generates table, dashboard, detail, workflow, and history views. | | process template | The versioned structure copied into a new tracked item. Editing it does not change existing items. | | tracked item | One independent instance of a process with its own template snapshot and optional customisation. | | standalone task | Independent work that does not require a process template. | | company | Lightweight context and future-order defaults, not a CRM record. | | order | A tracked item in the supported Orders process with purchase-specific fields and dates. | | stage | A workflow section containing checklist tasks. | | checklist task | One actionable step inside a stage or standalone task. | | Home | A bounded cross-process summary. | | Next Up | A computed queue of the most relevant open actions. It is read-only; mutate canonical resources. | | Attention | A computed queue of work needing intervention. | | archive | A recoverable lifecycle state removed from active views. | | permanent deletion | Irreversible removal after preview/confirmation where required. | Public documentation does not use database table, Convex document, CRM pipeline, assignee, team, or enterprise-administration language for these concepts. --- # Protocol and resource limits The capabilities response is the runtime source for enabled limits. OpenAPI schemas provide enforceable maxima. Initial v1 design limits are: - request body: 256 KiB; - JSON nesting depth: 20; schema-declared strings default to at most 20,000 characters, arrays to 1,000 entries, and extensible maps to 1,000 properties unless the operation declares a narrower limit. Numeric bounds stay within JavaScript's safe-integer magnitude. Arbitrary user-authored nested JSON remains subject to the total byte/depth ceilings even where its individual value schema is intentionally open; - default page: 50 items; maximum page: 100; - batch: at most 25 supported resource state actions; preview/apply commits all actions or none. See the [batch workflow example](/api/docs/workflows/recover-conflicts); - process dashboard: at most 100 distinct current-stage title groups per process. Identical bilingual stage titles share a group across item instances. Exceeding the bound returns actionable `422 atomic_scope_too_large` without partial writes; reduce distinct active stage labels or split the process. Counts are never silently truncated; - atomic resource scope: at most 1,000 loaded domain records and 2 MiB of canonical state, including embedded children; exceeding either bound returns 422 `atomic_scope_too_large` before applying changes. Split work into smaller resource operations. Paginated collection reads continue through their cursors; - idempotency key: 16-128 printable ASCII characters, retained at least 24 hours; - field lengths, collection sizes, and enums: the operation-specific OpenAPI request schema is authoritative; - per-key rate limits: 600 reads, 180 writes, or 30 destructive operations per rolling minute; owner-wide limits are three times the corresponding key limit. The runtime may advertise lower safety limits before general availability, but it must not silently exceed documented schema limits. Oversized bodies return 413; field and relationship limits return 422; throttling returns 429 with `Retry-After`. --- # API support Before requesting help, capture the operation ID, UTC time, HTTP status, stable problem `code`, and `X-Request-Id`. Never send an API key, Authorization header, idempotency record contents, workspace export, or private task/order content unless a secure support process explicitly requests the minimum required data. For API support, email [steadydoapp@gmail.com](mailto:steadydoapp@gmail.com). Send private security reports to [steadydoapp@gmail.com](mailto:steadydoapp@gmail.com) with the subject `SteadyDo security report`. Include a safe reproduction with fictional identifiers when possible. Do not post credentials or security reports to a public issue. The request ID supports correlation; it is not proof of success and does not authorize access. ## Service expectations The current API is a draft and is not production-certified. These draft documents publish no guaranteed uptime or support response time. Plan for temporary unavailability: honor `Retry-After` on throttling or service-unavailable responses, and follow the [retry and idempotency guidance](/api/docs/idempotency) before repeating a write whose outcome is unknown. See [protocol and resource limits](/api/docs/limits) for request, page and rate limits, and the operation reference for batch limits. --- # API v1 compatibility policy API v1 is identified by `/api/v1`. Stable operations follow these rules. Additive optional fields, new endpoints, new optional request properties, and new problem codes are normally non-breaking. Clients must ignore unknown response properties. New enum values are additive only when the schema explicitly marks the enum extensible; otherwise they require advance deprecation guidance. Removing or renaming an operation or field, changing a type or meaning, making optional input required, narrowing accepted values, or changing lifecycle effects is breaking and requires a new major API path. A stable operation is normally deprecated for at least 180 days. OpenAPI carries `deprecated`, replacement guidance, deprecation date, and sunset date; HTTP responses may also carry `Deprecation`, `Sunset`, and `Link` headers. Emergency security changes may shorten the window when continued behaviour creates material risk. The changelog explains the risk without disclosing exploit details, the migration path, and the exceptional schedule. Labels are `stable`, `beta`, `experimental`, and `deprecated`. Beta and experimental operations state their narrower guarantees in their OpenAPI description. No operation or field is currently deprecated. Future deprecated operations must set `deprecated: true` and `x-deprecation: {deprecatedAt: RFC3339 instant, sunsetAt: RFC3339 instant, replacement: HTTPS URL}`. Sunset must follow deprecation. The runtime validates dates and replacement URL, emits `Deprecation: @`, `Sunset: `, and a `Link` with `rel="successor-version"` on success and error responses. A changelog entry supplies the migration steps. Never invent a past deprecation or unsupported sunset promise. Workspace export is beta: approved content remains owner-scoped and secret-redacted, but record shapes and operational limits may change before stable release. See [export](/api/docs/export). Historical retention and deliberate Scalar updates follow [documentation maintenance](/api/docs/documentation-maintenance). --- # REST API changelog All dates use UTC. Entries are grouped as Added, Changed, Deprecated, Fixed, Security, or Removed. ## Unreleased ### 2026-09-20 — exhaustive acceptance corrections - Security: company template customization rejects identifiers belonging to system templates, other companies, or other processes. Authenticated adapters now enter shared domain operations with mandatory trusted actor context. - Fixed: company duplicate-name validation is shared by the app and REST. Task date/time fields can be explicitly cleared, and archived completed tasks retain their completion status. - Added: process lifecycle, company search/lifecycle, template company/status and Attention filters. Next Up returns typed task actions, all required parent IDs, and current version preconditions. Home queues share deterministic natural ordering and support localized workflow-title ordering. - Added: process and company dashboard aggregates, template field defaults and validated display settings with creation-time instance snapshots, and full/read-only/custom API-key presets with expiry and usage metadata. - Added: required scopes on permission failures, current version and ETag on stale writes, explicit mutation execution-outcome headers, and validated future deprecation metadata. Structured request logs allowlist correlation and outcome fields and exclude credentials. - Security: export records use explicit per-resource field allowlists, including nested deleted-process snapshots, rather than exposing new storage fields automatically. - Documentation: executable quickstart, expanded lifecycle and field guidance, raw HTTP samples, dependency and accessibility procedures, release metadata, revalidation policy, and browser coverage of search, examples, downloads and every operation. - Compatibility: these changes remain part of the unreleased draft. Passing local checks does not certify a production rollout, data migration, recovery exercise, or human review. ### 2026-09-20 — adapter and company validation corrections - Fixed: company custom fields conform to configured field definitions; default order templates must be owned, published Orders templates with matching company scope. Legacy and public field/default aliases remain synchronized. - Fixed: session creation accepts typed order/item custom fields. UI item creation includes required values in the initial atomic command, and UI order creation preserves its supplied custom fields. - Fixed: nested process page settings and process positions persist through both session and REST adapters; non-JSON view settings are rejected by the shared domain layer. - Fixed: field-definition PATCH accepts partial updates and preserves omitted properties. Required non-hidden fields remain visible during creation, including when older settings contain conflicting display flags. ### 2026-09-19 — parity and contract corrections - Fixed: process field reads expose persistent IDs and complete settings; all nine field types and visibility settings round-trip, including nested process creation. Unknown field properties are rejected instead of silently discarded. Field edits retain omitted labels, and archived process configuration rejects field/template mutations. - Added: process restoration can leave automatically archived items archived with `includeAutoArchivedItems: false`. - Fixed: session and REST company creation share structured contact/payment fields and defaults; session validators accept the workflow and process settings already supported by shared domain commands. - Changed: draft reorder requests for tasks, processes, process items, and workflow tasks require a complete `expectedVersions` member map. Missing member versions are rejected and stale member versions return 412 atomically. Fixed parentless process/order reorder If-Match targets (first submitted process and moving order); item creation uses its containing process version. The concurrency guide specifies every nested/header target. - Fixed: requirement IDs now match the supplied API plan (including task listing API-034 and workflow movement API-099), with multiple requirement links where one operation serves several acceptance items. - Added: standards-level OpenAPI 3.1 validation, validation of all named and embedded examples, documentation anchor/operation checks, enforced release notes, and scheduled anonymous artifact drift monitoring. - Added: bounded Home/summary section continuations with exact materialized counts. Projection preparation returns a retryable error rather than incomplete counts. - Fixed: lifecycle timestamps and unscheduled shipment/arrival dates are nullable in the downloadable contract, matching runtime validation. The private response-only schema overlay is removed. - Added: workflow-task deletion, process-item stage/task skip and unskip, workspace timezone preferences, process-field reordering, optional setup skipping, and saving an order workflow as a template. - Added: atomic batch preview/application for up to 25 supported task, order, and process-item actions. State-bound previews expire after ten minutes. - Changed: order updates accept owned company association (including null to clear it) and validated custom fields. Stage updates accept nullable explicit due dates. - Changed: template-application preview now accepts selected `itemIds` and reports the actual missing stages/tasks it will add. The formerly ineffective arbitrary `changes` payload is rejected; existing item edits and completed work are preserved. Stale selected-item versions invalidate the confirmation. - Changed: malformed template structures and invalid custom-field values now return validation errors. New items must provide visible, non-built-in required custom fields; unchanged legacy omissions on existing items remain editable. - Changed: collection cursors use indexed pagination. Restart an in-progress traversal when an old cursor is rejected after upgrading, and continue through empty pages while a continuation cursor exists. - Added: safe actor attribution on activity and bounded API-key pagination. The key list keeps its array shape; continuation is returned in `meta.nextCursor`. Embedded order history exposes an activity continuation cursor. - Security: key delegation cannot expand caller scopes or expiry; replay storage does not retain reusable key secrets. Recover a lost key-creation response through safe replacement, because replay returns `secretUnavailable`. - Fixed: published examples use valid strong ETags and describe nested workflow task/stage version requirements. Public bundles, runtime validators, operation index, and LLM documents derive from canonical YAML and fail freshness checks when stale. - Compatibility: these are corrections and additive operations within the draft v1 surface. Deployment certification is still required; this entry does not assert that an existing deployment already contains the changes. ### Added - Initial OpenAPI 3.1.1 authoring structure, protocol guidance, workflow guides, error registry, and fictional example conventions for the planned API v1 parity surface. No API v1 operation is certified for public production use until the parity registry, implementation conformance suite, and launch checklist are complete. --- # Documentation architecture decision Status: implemented local architecture decision; production certification remains separate. The supplied DOC-021 plan described an Astro/Vercel host. The current application is a TanStack Start React application emitted as a Cloudflare Worker; the independent marketing site uses Astro. Embedding the REST reference in the application uses the official `@scalar/api-reference-react@0.9.66`, pinned in package.json/package-lock.json, not the Astro package and not a CDN. This is the explicit implementation substitution for the obsolete host assumption, not a claim that an Astro/Vercel spike was performed. The reference fetches the canonical same-origin OpenAPI3.1.1 document without cookies, bundles Scalar and styles, disables telemetry/persistAuth/default remote fonts/agents, and uses the public auth boundary. Standards validation uses pinned `@scalar/openapi-validator@0.1.2` plus JSON Schema2020-12 example validation. Local emitted-Worker browser receipts exercise actual hydration, key entry/storage/network privacy, accessibility and recovery. Production scripts are restricted to self plus a response nonce, with no unsafe-eval or unsafe-inline script permission. Zod is configured jitless before Scalar schema construction because even a caught eval probe violates CSP. The style-src unsafe-inline exception is deliberately retained: Scalar and the application emit dynamic style attributes/style rules for component layout, theming and overlay positioning. Script execution remains nonce-restricted; styles cannot authorize external requests because connect/font/img policies remain constrained. The exception must be re-reviewed when upgrading Scalar, removing it when the upstream styling mechanism supports the nonce without breaking controls. This note records the engineering rationale, not a substitute for independent security review or production evidence. No unreviewed third-party proxy or telemetry destination is approved. Same-origin /api/v1 makes deployment infrastructure changes independent from documented URLs. Public support and private security-reporting contacts are published in [API support](/api/docs/support). Deployed conformance must still be verified before changing the draft stability/readiness language. --- # Error-code registry ```yaml version: 1 errors: - { code: invalid_request, status: 400, meaning: Request syntax or protocol metadata is invalid., retry: never_without_change, action: Correct malformed JSON, query syntax, or required headers., owner: request-boundary, operations: [all] } - { code: authentication_required, status: 401, meaning: Bearer credentials are missing or malformed., retry: after_credentials, action: Send a valid Bearer key., owner: authenticateApiKey, operations: [all-authenticated] } - { code: invalid_api_key, status: 401, meaning: The supplied key is unknown expired or revoked., retry: after_credentials, action: Replace an invalid expired or revoked key., owner: authenticateApiKey, operations: [all-authenticated] } - { code: insufficient_scope, status: 403, meaning: The key lacks a scope required by the matched operation., retry: after_authorization, action: Use a key with the required scope., owner: authorizeOperation, operations: [scope-declared-operations] } - { code: resource_not_found, status: 404, meaning: No visible resource or operation matches the supplied identifier., retry: never_without_change, action: Verify the opaque ID and resource type., owner: resolveOwnedResource, operations: [resource-detail-and-action-operations] } - { code: state_conflict, status: 409, meaning: The requested transition is illegal in current resource state., retry: after_reread, action: Read current state and choose an allowed action., owner: applyDomainCommand, operations: [lifecycle-and-workflow-mutations] } - { code: idempotency_conflict, status: 409, meaning: An idempotency key is already bound to a different request., retry: new_key_or_original_payload, action: Reuse the original payload or choose a new key., owner: enforceIdempotency, operations: [idempotent-mutations] } - { code: version_conflict, status: 412, meaning: If-Match does not equal the current resource version., retry: after_reread, action: Read the latest ETag reconcile and retry., owner: enforceVersion, operations: [versioned-mutations] } - { code: payload_too_large, status: 413, meaning: The HTTP body exceeds the public transport limit., retry: after_smaller_request, action: Reduce request or batch size., owner: readBoundedBody, operations: [body-bearing-operations] } - { code: unsupported_media_type, status: 415, meaning: A body-bearing request is not UTF-8 application/json., retry: after_header_change, action: Send UTF-8 application/json., owner: parseRequestBody, operations: [body-bearing-operations] } - { code: validation_failed, status: 422, meaning: The request is syntactically valid but violates its operation schema., retry: after_input_change, action: Correct the listed field violations., owner: validateOperationRequest, operations: [schema-constrained-operations] } - { code: atomic_scope_too_large, status: 422, meaning: Atomic dependency loading exceeded the documented row or byte bound., retry: after_smaller_request, action: Split work into smaller resource operations. No changes were applied., owner: loadAtomicScope, operations: [aggregate-and-configuration-mutations] } - { code: confirmation_expired, status: 422, meaning: A destructive confirmation is expired consumed or bound to different state., retry: after_new_preview, action: Request a new version-bound preview token., owner: verifyConfirmation, operations: [confirmed-destructive-mutations] } - { code: rate_limited, status: 429, meaning: The caller exceeded a key or owner rolling limit., retry: after_retry_after, action: Wait add jitter then retry only safe operations., owner: admitRequest, operations: [all-authenticated] } - { code: internal_error, status: 500, meaning: The server failed or produced a response outside the public contract., retry: safe_or_idempotent_only, action: Preserve the request ID and retry cautiously., owner: publicApiRuntime, operations: [all] } - { code: service_unavailable, status: 503, meaning: Routing authentication domain execution or a required projection is temporarily unavailable., retry: after_retry_after, action: Wait and retry safe or idempotent requests., owner: publicApiRuntime, operations: [all] } ``` # SteadyDo API operation index Release: 1.0.0-draft (2026-09-19). Generated from the canonical OpenAPI source. Source bc0eb62dacee6648b52146fd50672b9ff6ece74b (committed-release); [release metadata](/api/release.json). | Operation | Method/path | Purpose | Scopes | Destructive | Idempotency | Guide | | --- | --- | --- | --- | --- | --- | --- | --- | | `getCapabilities` | `GET /capabilities` | Discover API version, features, limits, held scopes, and documentation links. | workspace:read | no | no | [guide](/api/docs/quickstart) | | `getWorkspaceSummary` | `GET /workspace/summary` | Read a bounded orientation summary; follow collection links for complete data. | workspace:read | no | no | [guide](/api/docs/workflows/bootstrap-workspace) | | `exportWorkspace` | `GET /workspace/export` | Export all owner content and history, excluding secrets and operational records. | workspace:read | no | no | [guide](/api/docs/export) | | `getHome` | `GET /home` | Read the current cross-process Home projection. | workspace:read | no | no | [guide](/api/docs/workflows/work-next-up) | | `getNextUp` | `GET /next-up` | Read typed next actions; mutate their canonical resources, not this projection. | workspace:read | no | no | [guide](/api/docs/workflows/work-next-up) | | `getAttention` | `GET /attention` | Read work needing attention with reason, severity, and due metadata. | workspace:read | no | no | [guide](/api/docs/workflows/work-next-up) | | `listCompletedActivity` | `GET /completed` | List completed work with cursor pagination. | workspace:read | no | no | [guide](/api/docs/workflows/work-next-up) | | `getWeeklyActivity` | `GET /weekly` | Read activity for an explicit local calendar week. | workspace:read | no | no | [guide](/api/docs/workflows/work-next-up) | | `listApiKeys` | `GET /api-keys` | List key metadata without secrets or hashes; last-use data is debounced. | keys:manage | no | no | [guide](/api/docs/workflows/rotate-agent-key) | | `createApiKey` | `POST /api-keys` | Create a preserve-or-narrow successor and return its secret once. | keys:manage | no | required | [guide](/api/docs/workflows/rotate-agent-key) | | `getApiKey` | `GET /api-keys/{apiKeyId}` | Get safe key status, scopes, expiry, fingerprint, and approximate last use. | keys:manage | no | no | [guide](/api/docs/workflows/rotate-agent-key) | | `revokeApiKey` | `POST /api-keys/{apiKeyId}/revoke` | Immediately revoke an API key. | keys:manage | yes | required | [guide](/api/docs/workflows/rotate-agent-key) | | `rotateApiKey` | `POST /api-keys/{apiKeyId}/rotate` | Create a bounded successor and revoke the old key atomically or after explicit overlap. | keys:manage | yes | required | [guide](/api/docs/workflows/rotate-agent-key) | | `listTasks` | `GET /tasks` | List standalone tasks with supported filters and stable cursor pagination. | workspace:read | no | no | [guide](/api/docs/workflows/manage-standalone-work) | | `createTask` | `POST /tasks` | Create standalone work with optional company, schedule, notes, and checklist. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `reorderTasks` | `POST /tasks/reorder` | Reorder all non-archived standalone tasks without patching internal positions. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `getTask` | `GET /tasks/{taskId}` | Get a standalone task, checklist, history links, and allowed actions. | workspace:read | no | no | [guide](/api/docs/workflows/manage-standalone-work) | | `updateTask` | `PATCH /tasks/{taskId}` | Update caller-writable task fields without changing lifecycle state. | workspace:write | no | no | [guide](/api/docs/workflows/manage-standalone-work) | | `completeTask` | `POST /tasks/{taskId}/complete` | Complete a standalone task. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `reopenTask` | `POST /tasks/{taskId}/reopen` | Reopen a completed standalone task. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `pinTask` | `POST /tasks/{taskId}/pin` | Pin a standalone task. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `unpinTask` | `POST /tasks/{taskId}/unpin` | Unpin a standalone task. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `duplicateTask` | `POST /tasks/{taskId}/duplicate` | Duplicate a task as an independent copy. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `archiveTask` | `POST /tasks/{taskId}/archive` | Archive a standalone task. | archive:write | yes | required | [guide](/api/docs/workflows/manage-standalone-work) | | `restoreTask` | `POST /tasks/{taskId}/restore` | Restore an archived standalone task. | archive:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `previewTaskDeletion` | `POST /tasks/{taskId}/delete-preview` | Preview permanent task deletion without side effects. | destructive:write | no | no | [guide](/api/docs/workflows/manage-standalone-work) | | `permanentlyDeleteTask` | `POST /tasks/{taskId}/permanent-delete` | Permanently delete an archived task using a bound confirmation. | archive:write, destructive:write | yes | required | [guide](/api/docs/workflows/manage-standalone-work) | | `createTaskChecklistItem` | `POST /tasks/{taskId}/checklist-items` | Add a checklist item to a standalone task. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `reorderTaskChecklistItems` | `POST /tasks/{taskId}/checklist-items/reorder` | Reorder checklist items within their task aggregate. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `updateTaskChecklistItem` | `PATCH /tasks/{taskId}/checklist-items/{checklistItemId}` | Update caller-writable checklist fields. | workspace:write | no | no | [guide](/api/docs/workflows/manage-standalone-work) | | `deleteTaskChecklistItem` | `DELETE /tasks/{taskId}/checklist-items/{checklistItemId}` | Remove a checklist item from its task. | workspace:write | yes | required | [guide](/api/docs/workflows/manage-standalone-work) | | `completeTaskChecklistItem` | `POST /tasks/{taskId}/checklist-items/{checklistItemId}/complete` | Complete a standalone checklist item. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `reopenTaskChecklistItem` | `POST /tasks/{taskId}/checklist-items/{checklistItemId}/reopen` | Reopen a standalone checklist item. | workspace:write | no | required | [guide](/api/docs/workflows/manage-standalone-work) | | `getCompanyFields` | `GET /company-fields` | Read company field definitions and card visibility. | workspace:read | no | no | [guide](/api/docs/companies) | | `replaceCompanyFields` | `PUT /company-fields` | Replace company field/card configuration without changing related work. | config:write | no | no | [guide](/api/docs/companies) | | `listCompanies` | `GET /companies` | List lightweight company context with active/archive filters. | workspace:read | no | no | [guide](/api/docs/companies) | | `createCompany` | `POST /companies` | Create lightweight company context. | workspace:write | no | required | [guide](/api/docs/companies) | | `getCompany` | `GET /companies/{companyId}` | Get company context, related-resource links, and allowed actions. | workspace:read | no | no | [guide](/api/docs/companies) | | `updateCompany` | `PATCH /companies/{companyId}` | Update caller-writable company fields. | workspace:write | no | no | [guide](/api/docs/companies) | | `getCompanyDashboard` | `GET /companies/{companyId}/dashboard` | Read bounded summaries of work related to a company. | workspace:read | no | no | [guide](/api/docs/companies) | | `archiveCompany` | `POST /companies/{companyId}/archive` | Archive company context without archiving related work. | archive:write | yes | required | [guide](/api/docs/companies) | | `restoreCompany` | `POST /companies/{companyId}/restore` | Restore archived company context. | archive:write | no | required | [guide](/api/docs/companies) | | `previewCompanyDeletion` | `POST /companies/{companyId}/delete-preview` | Preview relationship effects without archiving or deleting related work. | destructive:write | no | no | [guide](/api/docs/companies) | | `permanentlyDeleteCompany` | `POST /companies/{companyId}/permanent-delete` | Permanently delete company context with a bound confirmation. | archive:write, destructive:write | yes | required | [guide](/api/docs/companies) | | `setCompanyOrderTemplate` | `PUT /companies/{companyId}/order-template` | Set or customise the template default for future orders only. | config:write | no | no | [guide](/api/docs/companies) | | `listProcesses` | `GET /processes` | List processes in navigation order with lifecycle filters. | workspace:read | no | no | [guide](/api/docs/processes-and-items) | | `createProcess` | `POST /processes` | Create a process and its standard generated views; setup may be completed or explicitly skipped. | config:write | no | required | [guide](/api/docs/processes-and-items) | | `ensureBaseConfiguration` | `POST /processes/bootstrap` | Idempotently ensure the supported base Orders process for a new workspace. | config:write | no | required | [guide](/api/docs/quickstart) | | `reorderProcesses` | `POST /processes/reorder` | Reorder process navigation without patching internal positions. | config:write | no | required | [guide](/api/docs/processes-and-items) | | `getProcess` | `GET /processes/{processId}` | Get process identity, configuration links, views, and allowed actions. | workspace:read | no | no | [guide](/api/docs/processes-and-items) | | `updateProcess` | `PATCH /processes/{processId}` | Update process name, icon, colour, and supported identity fields. | config:write | no | no | [guide](/api/docs/processes-and-items) | | `createProcessField` | `POST /processes/{processId}/fields` | Add a validated field definition for future and explicitly edited items. | config:write | no | required | [guide](/api/docs/fields-and-views) | | `updateProcessField` | `PATCH /processes/{processId}/fields/{fieldId}` | Update a field definition without deleting stored values. | config:write | no | no | [guide](/api/docs/fields-and-views) | | `hideProcessField` | `POST /processes/{processId}/fields/{fieldId}/hide` | Hide a field while retaining stored item values. | config:write | no | required | [guide](/api/docs/fields-and-views) | | `previewProcessFieldDeletion` | `POST /processes/{processId}/fields/{fieldId}/delete-preview` | Preview field definition and stored-item-data deletion. | destructive:write | no | no | [guide](/api/docs/fields-and-views) | | `permanentlyDeleteProcessField` | `POST /processes/{processId}/fields/{fieldId}/permanent-delete` | Permanently delete a field and its stored item values after confirmation. | config:write, destructive:write | yes | required | [guide](/api/docs/fields-and-views) | | `getProcessViews` | `GET /processes/{processId}/views` | Read generated table, dashboard, and detail display configuration. | workspace:read | no | no | [guide](/api/docs/fields-and-views) | | `updateProcessViews` | `PUT /processes/{processId}/views` | Replace generated view display configuration. | config:write | no | no | [guide](/api/docs/fields-and-views) | | `previewProcessArchive` | `POST /processes/{processId}/archive-preview` | Preview active-item and navigation effects of archiving a process. | archive:write | no | no | [guide](/api/docs/processes-and-items) | | `archiveProcess` | `POST /processes/{processId}/archive` | Archive a process using the exact previewed scope. | archive:write | yes | required | [guide](/api/docs/processes-and-items) | | `restoreProcess` | `POST /processes/{processId}/restore` | Restore an archived process and its recoverable structure. | archive:write | no | required | [guide](/api/docs/processes-and-items) | | `previewProcessDeletion` | `POST /processes/{processId}/delete-preview` | Preview process, item, workflow, and snapshot deletion effects. | destructive:write | no | no | [guide](/api/docs/processes-and-items) | | `permanentlyDeleteProcess` | `POST /processes/{processId}/permanent-delete` | Permanently delete a process and confirmed snapshots. | archive:write, destructive:write | yes | required | [guide](/api/docs/processes-and-items) | | `listProcessTemplates` | `GET /processes/{processId}/templates` | List draft, published, and archived process templates. | workspace:read | no | no | [guide](/api/docs/workflows/maintain-template) | | `createProcessTemplate` | `POST /processes/{processId}/templates` | Create a draft template for future tracked items. | config:write | no | required | [guide](/api/docs/workflows/maintain-template) | | `getProcessTemplate` | `GET /processes/{processId}/templates/{templateId}` | Get a versioned template structure and allowed actions. | workspace:read | no | no | [guide](/api/docs/workflows/maintain-template) | | `updateProcessTemplate` | `PATCH /processes/{processId}/templates/{templateId}` | Update a template; existing tracked items remain unchanged. | config:write | no | no | [guide](/api/docs/workflows/maintain-template) | | `duplicateProcessTemplate` | `POST /processes/{processId}/templates/{templateId}/duplicate` | Duplicate a template into an independent draft. | config:write | no | required | [guide](/api/docs/workflows/maintain-template) | | `publishProcessTemplate` | `POST /processes/{processId}/templates/{templateId}/publish` | Publish a validated template version for future items. | config:write | no | required | [guide](/api/docs/workflows/maintain-template) | | `archiveProcessTemplate` | `POST /processes/{processId}/templates/{templateId}/archive` | Archive a template without changing existing item snapshots. | config:write, archive:write | yes | required | [guide](/api/docs/workflows/maintain-template) | | `setDefaultProcessTemplate` | `POST /processes/{processId}/templates/{templateId}/set-default` | Select the default for future items only. | config:write | no | required | [guide](/api/docs/workflows/maintain-template) | | `previewTemplateApplication` | `POST /processes/{processId}/templates/{templateId}/apply-preview` | Preview selected template changes against an explicit tracked-item scope. | config:write | no | no | [guide](/api/docs/workflows/maintain-template) | | `applyTemplateToItems` | `POST /processes/{processId}/templates/{templateId}/apply` | Apply only previewed changes to the confirmed selected item scope. | config:write, workspace:write | yes | required | [guide](/api/docs/workflows/maintain-template) | | `listProcessItems` | `GET /processes/{processId}/items` | List tracked items with stable filters and pagination. | workspace:read | no | no | [guide](/api/docs/processes-and-items) | | `createProcessItem` | `POST /processes/{processId}/items` | Create a tracked item with an independent selected/default template snapshot. | workspace:write | no | required | [guide](/api/docs/processes-and-items) | | `reorderProcessItems` | `POST /processes/{processId}/items/reorder` | Reorder tracked items. | workspace:write | no | required | [guide](/api/docs/processes-and-items) | | `getProcessItem` | `GET /processes/{processId}/items/{itemId}` | Get item fields, template snapshot, workflow, history links, and allowed actions. | workspace:read | no | no | [guide](/api/docs/processes-and-items) | | `updateProcessItem` | `PATCH /processes/{processId}/items/{itemId}` | Update item core/custom fields without changing its source template. | workspace:write | no | no | [guide](/api/docs/processes-and-items) | | `completeProcessItem` | `POST /processes/{processId}/items/{itemId}/complete` | Complete a tracked item. | workspace:write | no | required | [guide](/api/docs/processes-and-items) | | `reopenProcessItem` | `POST /processes/{processId}/items/{itemId}/reopen` | Reopen a completed tracked item. | workspace:write | no | required | [guide](/api/docs/processes-and-items) | | `duplicateProcessItem` | `POST /processes/{processId}/items/{itemId}/duplicate` | Duplicate an item and its workflow snapshot independently. | workspace:write | no | required | [guide](/api/docs/processes-and-items) | | `previewProcessItemDivision` | `POST /processes/{processId}/items/{itemId}/divide-preview` | Preview division results without side effects. | workspace:write | no | no | [guide](/api/docs/processes-and-items) | | `divideProcessItem` | `POST /processes/{processId}/items/{itemId}/divide` | Divide an item using an approved preview. | workspace:write | yes | required | [guide](/api/docs/processes-and-items) | | `archiveProcessItem` | `POST /processes/{processId}/items/{itemId}/archive` | Archive a tracked item. | archive:write | yes | required | [guide](/api/docs/processes-and-items) | | `restoreProcessItem` | `POST /processes/{processId}/items/{itemId}/restore` | Restore an archived tracked item. | archive:write | no | required | [guide](/api/docs/processes-and-items) | | `previewProcessItemDeletion` | `POST /processes/{processId}/items/{itemId}/delete-preview` | Preview item/workflow/history deletion. | destructive:write | no | no | [guide](/api/docs/processes-and-items) | | `permanentlyDeleteProcessItem` | `POST /processes/{processId}/items/{itemId}/permanent-delete` | Permanently delete an archived item after confirmation. | archive:write, destructive:write | yes | required | [guide](/api/docs/processes-and-items) | | `createProcessItemStage` | `POST /processes/{processId}/items/{itemId}/stages` | Add an item-specific stage without editing its source template. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `updateProcessItemStage` | `PATCH /processes/{processId}/items/{itemId}/stages/{stageId}` | Update an item-specific stage. | workspace:write | no | no | [guide](/api/docs/workflow-states) | | `completeProcessItemStage` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/complete` | Complete an allowed item stage. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `reopenProcessItemStage` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/reopen` | Reopen a completed item stage. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `createProcessItemTask` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/tasks` | Add an item-specific checklist task. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `reorderProcessItemTasks` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/tasks/reorder` | Reorder tasks in an item stage. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `updateProcessItemTask` | `PATCH /processes/{processId}/items/{itemId}/stages/{stageId}/tasks/{workflowTaskId}` | Update an item-specific checklist task and schedule. | workspace:write | no | no | [guide](/api/docs/workflow-states) | | `deleteProcessItemTask` | `DELETE /processes/{processId}/items/{itemId}/stages/{stageId}/tasks/{workflowTaskId}` | Delete an individual process-item workflow task and recompute its stage. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `completeProcessItemTask` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/tasks/{workflowTaskId}/complete` | Complete an allowed item checklist task. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `reopenProcessItemTask` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/tasks/{workflowTaskId}/reopen` | Reopen a completed item checklist task. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `pinProcessItemTask` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/tasks/{workflowTaskId}/pin` | Pin an item checklist task. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `unpinProcessItemTask` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/tasks/{workflowTaskId}/unpin` | Unpin an item checklist task. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `listOrders` | `GET /orders` | List active, completed, or archived orders with stable filters. | workspace:read | no | no | [guide](/api/docs/workflows/create-operate-order) | | `createOrder` | `POST /orders` | Create an order from explicit, company-future-default, or process-default template precedence. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `reorderOrders` | `POST /orders/reorder` | Reorder orders without patching internal positions. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `getOrder` | `GET /orders/{orderId}` | Get specialised purchase fields, workflow snapshot, derived dates, and allowed actions. | workspace:read | no | no | [guide](/api/docs/workflows/create-operate-order) | | `updateOrder` | `PATCH /orders/{orderId}` | Update caller-writable order fields and recalculate documented derived dates. | workspace:write | no | no | [guide](/api/docs/workflows/create-operate-order) | | `completeOrder` | `POST /orders/{orderId}/complete` | Complete an order when workflow preconditions allow. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `reopenOrder` | `POST /orders/{orderId}/reopen` | Reopen a completed order. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `duplicateOrder` | `POST /orders/{orderId}/duplicate` | Duplicate an order and workflow snapshot independently. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `previewOrderDivision` | `POST /orders/{orderId}/divide-preview` | Preview order division and resulting quantities/workflows. | workspace:write | no | no | [guide](/api/docs/workflows/create-operate-order) | | `divideOrder` | `POST /orders/{orderId}/divide` | Divide an order using an approved preview. | workspace:write | yes | required | [guide](/api/docs/workflows/create-operate-order) | | `archiveOrder` | `POST /orders/{orderId}/archive` | Archive an order while retaining workflow and history. | archive:write | yes | required | [guide](/api/docs/workflows/create-operate-order) | | `restoreOrder` | `POST /orders/{orderId}/restore` | Restore an archived order. | archive:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `previewOrderDeletion` | `POST /orders/{orderId}/delete-preview` | Preview permanent order/workflow/history deletion. | destructive:write | no | no | [guide](/api/docs/workflows/create-operate-order) | | `permanentlyDeleteOrder` | `POST /orders/{orderId}/permanent-delete` | Permanently delete an archived order after confirmation. | archive:write, destructive:write | yes | required | [guide](/api/docs/workflows/create-operate-order) | | `createOrderStage` | `POST /orders/{orderId}/stages` | Add an order-specific stage without changing its template. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `updateOrderStage` | `PATCH /orders/{orderId}/stages/{stageId}` | Update order-stage fields and derived scheduling. | workspace:write | no | no | [guide](/api/docs/workflows/create-operate-order) | | `completeOrderStage` | `POST /orders/{orderId}/stages/{stageId}/complete` | Complete an allowed order stage and open eligible successors. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `reopenOrderStage` | `POST /orders/{orderId}/stages/{stageId}/reopen` | Reopen a completed order stage. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `skipOrderStage` | `POST /orders/{orderId}/stages/{stageId}/skip` | Mark an allowed order stage not required. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `unskipOrderStage` | `POST /orders/{orderId}/stages/{stageId}/unskip` | Restore a skipped order stage to required work. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `createOrderTask` | `POST /orders/{orderId}/stages/{stageId}/tasks` | Add an order-specific task and schedule. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `reorderOrderTasks` | `POST /orders/{orderId}/stages/{stageId}/tasks/reorder` | Reorder tasks within an order stage. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `updateOrderTask` | `PATCH /orders/{orderId}/stages/{stageId}/tasks/{workflowTaskId}` | Update order task content/schedule and recalculate deadlines. | workspace:write | no | no | [guide](/api/docs/workflows/create-operate-order) | | `deleteOrderTask` | `DELETE /orders/{orderId}/stages/{stageId}/tasks/{workflowTaskId}` | Delete an individual order workflow task and recompute its stage. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `moveOrderTask` | `POST /orders/{orderId}/stages/{stageId}/tasks/{workflowTaskId}/move` | Move a task to a validated stage/position. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `completeOrderTask` | `POST /orders/{orderId}/stages/{stageId}/tasks/{workflowTaskId}/complete` | Complete an allowed order task. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `reopenOrderTask` | `POST /orders/{orderId}/stages/{stageId}/tasks/{workflowTaskId}/reopen` | Reopen a completed order task. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `skipOrderTask` | `POST /orders/{orderId}/stages/{stageId}/tasks/{workflowTaskId}/skip` | Mark an allowed order task not required. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `unskipOrderTask` | `POST /orders/{orderId}/stages/{stageId}/tasks/{workflowTaskId}/unskip` | Restore a skipped order task to required work. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `pinOrderTask` | `POST /orders/{orderId}/stages/{stageId}/tasks/{workflowTaskId}/pin` | Pin an order task. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `unpinOrderTask` | `POST /orders/{orderId}/stages/{stageId}/tasks/{workflowTaskId}/unpin` | Unpin an order task. | workspace:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `listOrderActivity` | `GET /orders/{orderId}/activity` | List immutable order history events in stable order. | workspace:read | no | no | [guide](/api/docs/workflows/archive-recover) | | `listProcessItemActivity` | `GET /processes/{processId}/items/{itemId}/activity` | List immutable tracked-item history events. | workspace:read | no | no | [guide](/api/docs/workflows/archive-recover) | | `listTaskActivity` | `GET /tasks/{taskId}/activity` | List immutable standalone-task history events. | workspace:read | no | no | [guide](/api/docs/workflows/archive-recover) | | `listArchive` | `GET /archive` | List archived resources with typed restore and deletion links. | workspace:read | no | no | [guide](/api/docs/workflows/archive-recover) | | `skipProcessItemStage` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/skip` | Mark an allowed process-item stage not required. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `unskipProcessItemStage` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/unskip` | Restore a skipped process-item stage to required work. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `skipProcessItemTask` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/tasks/{workflowTaskId}/skip` | Mark an allowed process-item task not required. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `unskipProcessItemTask` | `POST /processes/{processId}/items/{itemId}/stages/{stageId}/tasks/{workflowTaskId}/unskip` | Restore a skipped process-item task to required work. | workspace:write | no | required | [guide](/api/docs/workflow-states) | | `getPreferences` | `GET /preferences` | Read the persisted workspace timezone used by date-based queues. | workspace:read | no | no | [guide](/api/docs/workflows/bootstrap-workspace) | | `updatePreferences` | `PATCH /preferences` | Set the workspace IANA timezone for local dates and date-based queues. | workspace:write | no | required | [guide](/api/docs/workflows/bootstrap-workspace) | | `reorderProcessFields` | `POST /processes/{processId}/fields/reorder` | Reorder all visible process fields using their exact active sibling set. | config:write | no | required | [guide](/api/docs/fields-and-views) | | `skipProcessSetup` | `POST /processes/{processId}/skip-setup` | Mark optional process setup as skipped without creating tracked work. | config:write | no | required | [guide](/api/docs/processes-and-items) | | `saveOrderAsTemplate` | `POST /orders/{orderId}/save-template` | Save the order workflow structure as a new published template for future work. | config:write | no | required | [guide](/api/docs/workflows/create-operate-order) | | `previewBatchActions` | `POST /batch/preview` | Preview up to 25 resource actions before applying them atomically. | workspace:write | no | required | [guide](/api/docs/workflows/bootstrap-workspace) | | `applyBatchActions` | `POST /batch/apply` | Apply every action from an unexpired batch preview in one transaction. | workspace:write | no | required | [guide](/api/docs/workflows/bootstrap-workspace) |