This changelog tracks notable backwards-compatible and breaking changes to Manifestly API v2.
deprecated: true in the OpenAPI schema.roles_to_users on POST /runs now accepts user_id and membership_id (additive). An item names the role with role_id and the principal with membership_id, user_id or email; when more than one is sent, that is the order of precedence. membership_id is the identifier POST /runs/{run_id}/run_steps/{id}/assign already takes, and user_id is the id the members of GET /roles report. Either one that does not resolve to a member of the run’s department is refused with 404, where an unmatched email still invites a limited member.GET /roles as a member of the very role being mapped — was never found, and the request fell through to inviting the agent’s address, which 404’d because that address is synthetic. Agents now resolve by user_id, by membership_id, and by the synthetic email GET /roles reports. A workflow whose steps are role-assigned to an agent can be started through the API, which is what makes it reachable from a scheduled run or any API-driven trigger. API keys are still refused: a role cannot be mapped onto one.email beginning invalid: that matches no member of the run’s department is refused with 404 instead of being handed to user creation.roles_to_users in run payloads is documented as the object it has always been (documentation fix, no behavior change). The schema declared an array of {role_id, email}; the field is rendered as an object keyed by the index of the assignment, whose values are {role_id, user_id} with both ids as strings. Anything generated from the old declaration never matched a real response.POST /runs documents its 404 (documentation fix, no behavior change). A roles_to_users or participants entry naming somebody the request cannot resolve has always answered {"error": "Participant not found", "message": ...}; the operation declared no 404 at all.PATCH /workflows/{id} now accepts title and description (additive). The endpoint previously accepted business_calendar_id and business_day_rule alone, so a client could rewrite every step of a workflow and could not change its name: title was refused as an unpermitted parameter, which is what the published 422 example for this operation showed. Both are plain attributes with no feature gate, so neither adds a 403. A blank, whitespace-only or over-255-character title is refused with 422 and the stored title is unchanged; description accepts null to clear it, and is rendered through the same sanitizer as the workflow editor (URLs become links, HTML other than b, i and u is stripped). A locked shared copy still refuses every key with 422, as it did before. Every other key is still refused as an unpermitted parameter.POST/PATCH on steps, content objects and step data settings answer 409 for a workflow that has versions; this endpoint does not, and did not for the calendar keys either. One consequence is worth knowing: promoting a draft rewrites the live workflow’s description from the draft’s snapshot, so a description set here on a version-controlled workflow is replaced at the next promote. title is not — promotion deliberately leaves it alone.GET /users now lists AI agents alongside people (behavior change; response shape unchanged). The listing previously returned people only, so an AI agent’s membership id, which POST /runs/{run_id}/run_steps/{id}/assign already accepts, could not be discovered. GET /users/{id} now resolves an agent’s id instead of answering 404. API keys are still never listed.GET /users accepts population (additive): all (the default) returns people and AI agents, people returns people only (what the listing returned before this change), ai_agents returns AI agents only. Any other value is refused with 400 rather than ignored.type (additive): User for a person, AiAgent for an AI agent. It appears in GET /users, GET /users/{id}, the members of GET /roles and the members of GET /departments/{id}. An agent’s email is synthetic and cannot receive mail, so check type before treating email as a way to reach a member.message and errors render full_messages, so a message written as a fragment appended to a humanized attribute name restated the attribute and never said what the rule was. Two strings change:
A section cannot be nested inside another section — was Parent step cannot have a parent, returned by POST/PATCH on run steps and workflow steps when a section is given a parent.A step cannot be nested inside itself — was Parent run step cannot parent self (run steps) and Parent step cannot parent self (workflow steps).Anything string-matching the previous text needs updating; the status code, the error label and every other field are unchanged.
kind, the due date and subtasks are now writable (additive). POST /workflows/{workflow_id}/steps and PATCH /workflows/{workflow_id}/steps/{id} accept kind (standard or stop_task), late_at_basis, late_at_offset, late_at_offset_units, late_at_offset_units_mode, subtasks (an array of up to 100 titles that replaces the step’s subtasks, matching an unchanged title to the subtask that already carries it so a run in progress keeps that subtask’s completion, and renaming the rest in order; null or [] removes them) and subtasks_required. These are the step attributes the workflow import already accepts, mapped the same way. late_at_basis: null clears the due date. kind: approval is refused with 422, because approval steps need reviewed steps that are chosen in the workflow editor, and so is a step whose approval is already configured; an unknown kind is refused with 422 as well, and so is a section (header_step: true) carrying due-date or subtask keys; a section may still be a stop_task. late_at_basis: date_data_setting is refused with 422: the data setting a due date keys off is chosen in the workflow editor. Every one of these keys was previously refused as an unpermitted parameter.subtasks_required (additive) in every step payload, in the steps endpoints and in the steps embedded in GET /workflows/{id}, so the flag can be read back once set.Idempotency-Key is now honored on PUT and DELETE (additive). Only POST and PATCH acted on the header, though the guarantee is installed on every v2 write. A PUT or DELETE carrying a key now replays the original 2xx — including a 204 — so a retry that lost its response can tell “I already succeeded” from “this never existed” rather than reading the 404 a destroyed record answers. Requests that send no key are unaffected.Idempotency-Key (documentation fix, no behavior change). Ten operations declared it; the rest honored it silently. All 47 POST/PATCH/PUT/DELETE operations now declare the same optional header, so retry-safety reads as the platform guarantee it is instead of a property of a handful of endpoints.required (additive). required has always been writable on POST/PATCH /workflows/{workflow_id}/steps/{step_id}/data_settings and was never returned, so a client could set it and had no way to read it back. It is now part of every data setting payload — the data_settings endpoints, and the step_data_setting embedded in GET /workflows/{id}, GET /workflows/{id}/steps and GET /workflows/{id}/steps/{id}.StepDataSetting component and now references it, so the two can no longer drift. The copy documented eleven formats; value_format now documents datetime and choose_from_data_table as well. The copy also marked value_format_config required, which the component does not.step_data_setting is documented as always present (documentation fix, no behavior change). The previous description said a step carrying more than one field returns step_data_settings instead. It returns both: step_data_setting always carries the first setting, and step_data_settings is added only when the step carries more than one.data_collection documents the two formats it was missing (documentation fix, no behavior change). datetime joins the DataCollectionOther format list, and choose_from_data_table gets its own variant carrying data_table_id and display_column_id. A run step collecting either previously matched no documented variant.{"error": "<category>", "message": "<detail>"}; a model validation failure answers {"errors": ["<message>", ...]}, a flat array. Both were already the majority shape; these endpoints were the exceptions:
POST /runs on a validation failure returned errors keyed by field ({"errors": {"checklist": ["must exist"]}}) and, when the run could not be built at all, a bare string. It now returns the flat array every other endpoint returns.checklist_id on run create returned {"errors": ["Checklist not found"]}. It now returns {"error": "Resource not found"}, byte-identical to a missing id, so the two cases can no longer be told apart.POST/PATCH /workflows/{workflow_id}/steps, POST/PATCH /workflows/{workflow_id}/steps/{step_id}/content_objects, PATCH /workflows/{id} and PATCH /runs/{id} returned message alone. They now also carry error: "Unpermitted parameters", matching every other unpermitted-parameter response. A malformed wrapper ({"step": "not an object"}) answers error: "Invalid parameter" rather than claiming an unpermitted key.Error is the refusal envelope, ValidationErrors is the validation envelope. Error now requires error, carries message, and no longer declares errors; the field-keyed form it described is emitted nowhere. Endpoints that answer with the flat array document ValidationErrors, and a status that can answer either documents both. Every documented 4xx/5xx now references one of the two (or an extension of Error, as the approval-decision blockers body does) rather than re-typing the shape: two 422s that declared an equivalent object inline now reference ValidationErrors, and five error responses that declared no body at all (GET /assignments 400, GET /workflows/{workflow_id}/fields/{id} 404, POST /workflows/{id}/export_csv 404, GET /workflows/{id}/export_status 404, POST /workflows/import/validate 422) now declare the body they were already returning. Generated clients get a resolvable failure type on all of them.Error body.POST /runs/{run_id}/run_steps/{run_step_id}/comments described a body the endpoint has never returned. It now shows the real one./workflows/{workflow_id}/steps/{step_id}/data_settings now behaves like the two sibling authoring endpoints (/steps and /steps/{step_id}/content_objects), and like /runs/{run_id}/run_steps/{run_step_id}/fields on the run side:
GET on the collection now lists. It returns {"data_settings": [...]} in position order, and an empty array for a step carrying none. It previously returned {"data_setting": {...}} for the lowest-position field, and 404 when the step had none.PATCH and DELETE on the collection path are gone. Address the field directly: GET, PATCH and DELETE /workflows/{workflow_id}/steps/{step_id}/data_settings/{id}. These were already routed but undocumented; they are now the only way to read, change or delete one field. Ids come from the collection listing or from GET /workflows/{workflow_id}/steps/{step_id}. This removes a silent guess: on a step with several fields, the id-less forms acted on the lowest-position one and answered 200 either way.POST is unchanged, on the same collection path, since it names no existing field.POST, PATCH and DELETE now refuse a workflow the authoring API does not expose: 404 for an inactive, archived or non-recurring workflow (single-run containers and step libraries, which GET /workflows/{id} already answers 404 for), 403 for a locked shared copy, and 409 for a workflow under version control. Previously any of these could be edited through this endpoint alone._links.self on a data setting now points at /data_settings/{id} rather than the collection path, so following it resolves one field. This link is also embedded in step payloads, so it changes in GET /workflows/{id}, GET /workflows/{id}/steps and GET /workflows/{id}/steps/{id} as well.data_tables feature gate applies to PATCH, not just POST. An account without the feature can no longer reach value_format: choose_from_data_table by creating a text field and then converting it.mode query parameter was documented backwards (documentation fix, no behavior change). Omitting mode returns top-level steps only; flat is what returns every step. The schema previously described flat as the default.schema_version bumped to 5). Two orthogonal business-day controls extend beyond recurrence to step due dates and reminders:
business_day_rule (none / next_business_day / previous_business_day, or null): when a computed run/step due date or reminder lands on a non-business day, it moves to the next/previous business day. Writable via PATCH /workflows/{id} (requires the business_day_snap feature — a loud 403 without it; unknown values 422). Applies to runs started or synced after the rule is set; in-flight runs keep their mirrored rule.expected_duration_units_mode / open_duration_units_mode, and workflow step objects gain the due-date offset fields late_at_basis, late_at_offset, late_at_offset_units plus late_at_offset_units_mode (all read-only in v2 responses). A mode of business skips non-business days while counting a days offset; regular is plain calendar arithmetic. Absent an explicit mode, it derives from the units (days implies business) — the historical behavior, unchanged.GET /api/v2/schemas/checklist.json): optional checklist.business_day_rule, checklist.expected_duration_units_mode, per-step late_at_offset_units_mode, and per-reminder reminder_at_offset_units_mode. The auto-close settings also join the contract: checklist.open_duration, open_duration_units, and open_duration_units_mode (previously column-only; an absent key leaves an existing workflow’s auto-close unchanged when syncing, an explicit null duration disables it). The schema is now schema_version 5 — emit 5. In a v5 document an omitted mode means “author omitted an optional field”: newly imported records derive it from the units (days implies business), synced records keep their stored mode. Documents at schema_version <= 4 (stored workflow versions, old exports) are upgraded on import: the derived mode is stamped next to every offset-units key they carry, since days units always meant business counting before these fields existed — so restoring a pre-mode snapshot restores business counting even where a step has since been set to explicit regular. On import/sync the mode and rule keys are dropped when the business_day_snap feature is not active, so such documents import exactly as before the fields existed. On version rollback an absent business_day_rule leaves the current rule unchanged while an explicit null clears it.schema_version stays 4). The checklist JSON contract (GET /api/v2/schemas/checklist.json) gains optional checklist.business_calendar_id (integer or null): the account holiday calendar attached to the workflow. On import the id is resolved within the importing master account (requires the business_calendars feature); an id that does not resolve there — a document exported from another account, or a since-deleted calendar — is skipped and the import proceeds, so calendar attachments do not transfer across accounts. On version rollback an absent key (documents predating the field) leaves a workflow’s live attachment untouched, while an explicit null detaches. Backwards-compatible, so no schema version bump. Calendars themselves are managed via the new /business_calendars endpoints.Data settings — primary fully retired: workflow (step) responses and the checklist JSON schema (breaking). Following the run-step removal below, the primary boolean is gone from the remaining surfaces: v1 workflow/step responses (step_data_settings[] entries and the step data-setting endpoints) and the public checklist JSON contract. GET /api/v2/schemas/checklist.json is now schema_version 4, which no longer defines the attribute; documents at schema_version <= 3 (stored workflow versions, old exports) are upgraded on import — any primary keys they carry are stripped. Emit 4 and omit primary when generating documents; position is the ordering contract.
Data settings — primary removed from run step responses (breaking). The primary boolean no longer appears in data_collection.data_settings[] entries on run step responses (GET /runs/{id}, GET /runs/{run_id}/run_steps, GET /runs/{run_id}/run_steps/{id}, and assignment run steps). The flag marked which setting backed the legacy single-value surfaces; that concept is retired — a step’s data settings are an ordered collection, and position is the ordering contract. The data_collection top-level required/value_format/config continue to describe the step’s first setting by position (previously the primary-flagged one — the same setting in practice). Multi-setting steps are unaffected where it matters: fields are addressed stably by field_id via the fields endpoints, which never exposed primary.
GET /runs/{id}/summary now includes visible-locked steps (behavior change). The summary endpoint previously filtered on .applicable and silently dropped every step behind a stop_task or approval gate — a section made up of only a locked approval, for example, disappeared from the summary entirely. The endpoint now matches the web run UI and includes those steps (they render locked in the UI, and continue to reject complete/skip until the gate resolves), keeping the v2 read-surface contract that summary reflects what the user sees. Progress percentages and the summary’s per-section counts now include locked steps in the denominator, so the reported completion percent matches the on-screen bar. No response-shape changes.can_approve / approve_blocked_reason (additive; new fields + new 422 reasons). Approval run step and assignment responses now include can_approve (boolean) and approve_blocked_reason alongside the decide and retract pairs. Approving completes the step, so an approved decision has preconditions beyond decidability: required data filled, required subtasks complete, stored number values within their constraints. can_decide continues to describe an approve-or-reject call and can be true while can_approve is false — rejection stays available with those preconditions unmet. Backed by RunStep#approve_blockers, the same code path Reviews::Decide enforces, so the advertised signal matches the server’s answer. Present only on kind: approval steps for accounts with the approvals feature flag.
decision: "approved" (same error + blockers shape as other decision blockers): required_data_missing, required_subtasks_incomplete, number_constraint_violated. Previously these cases wrote a Review, rolled it back, and returned generic prose (Failed to record decision); they are now refused up front with the named reason and no Review write.stale review is recorded and the step reopens, the same cascade as when reviewed work changes. Previously the approval silently remained in effect over blank required data.Runs — manual complete / reopen via state (additive; new accepted parameter + link changes). PATCH /runs/{id} now accepts run.state: "completed" manually completes a started run (requires every applicable step to be complete, else 422 — same bookkeeping as auto-completion), and "started" reopens a completed or closed run (matching the web uncomplete/reopen actions). Accepted values match serializer output, so a response’s state can be echoed back verbatim; "active" is rejected. _links changes: complete now appears only while the run is started (it previously also appeared on completed runs), and a reopen link (body {"state": "started"}) appears on completed and closed runs.
Approvals — can_retract / retract_blocked_reason and retract link (additive; new fields + link). Approval run step and assignment responses now include can_retract (boolean) and retract_blocked_reason ("not_approved_yet", "not_eligible_to_retract", or null) alongside can_decide / decision_blocked_reason, and _links carries a retract entry when the STATE allows retraction (the latest review is currently approved). Both are backed by RunStep#retract_blockers — the same code path Reviews::Decide enforces on decision: "retracted" — so the advertised signal is guaranteed to match the server’s answer. Present only on kind: approval steps for accounts with the approvals feature flag. Mirrors the decide-link convention: the link is state-scoped (documents that retraction is possible here), can_retract is actor-scoped (predicts success for the authenticated user), and the 422 explains refusals in the same vocabulary.
decision value). POST /runs/{run_id}/run_steps/{id}/decision now accepts decision: "retracted" alongside approved/rejected. Retracting withdraws an approval that is currently in effect — the step’s latest review is approved — and returns the approval to undecided (its step reopens) so it can be decided afresh. A required comment still applies. The withdrawn approval and the retraction are both preserved in the append-only review history.
can_decide / decision_blocked_reason continue to describe an approved/rejected call only — retract availability is advertised through can_retract / retract_blocked_reason and the retract link.error + blockers shape as other decision blockers): not_approved_yet (the step is not currently approved — never approved, or already rejected, stale, or retracted) and not_eligible_to_retract (caller is neither the original approver nor an admin on an unassigned approval).400 Invalid decision message now reads Decision must be one of approved, rejected, retracted. stale remains system-generated (written when reviewed work changes after approval) and is still rejected on this endpoint.POST /runs/{run_id}/run_steps/{id} with action: "skip") now always returns 422 with approval run steps cannot be skipped directly. To reopen an approved approval, change the reviewed work (stale cascade) or record a new decision (retraction). Previously a pending approval’s refusal carried a malformed message (" cannot be skip"), and skipping an already-decided approval returned 200 “skipped successfully” without changing anything — a no-op reported as success.gates_downstream holds every step after it, exactly as a stop task does: those steps report blocked_kind: "approval" with blocking_run_step_id pointing at the approval, render as locked, and reject complete/skip with 422 until the approval is approved. A rejected or still-undecided approval keeps holding them — completion only happens on approval.
POST /runs/{run_id}/run_steps/{id}/decide), not completing a stop task. Agents that fetch blocking_run_step_id should branch on the blocker’s kind.blocked_kind: null.review_configuration.gates_downstream (boolean) in the checklist JSON contract (/api/v2/schemas/checklist.json, now schema_version 3). Omitting it in a v3 document means true; documents at schema_version 2 or lower are upgraded on import with gates_downstream: false, preserving their original behavior.GET /roles endpoint: lists roles in active departments the token’s user administers, one entry per department; tokens without admin access receive a 200 with an empty list (not 403), so an empty result can mean either no roles or no admin visibility. Response includes id, name, initiator (true for the reserved Initiator sentinel), department_id, department_name, and members (id/email/name for users attached via roles_memberships). Role names are unique per department only — duplicate names across departments are legal — so callers should key on id and disambiguate via department_id/department_name. Supports page, per_page, search (case-insensitive on name), and department_id query params. Role IDs are valid assignee_id values when assignee_type: "Role" (workflow import) and in roles_to_users on run creation. Follow-up: the Manifestly MCP server will proxy this as a list_roles tool.blocked_kind replaces blocked_by_stop_task (breaking; three days after the boolean shipped). Run-step and assignment responses (GET /runs/{id}, GET /runs/{run_id}/run_steps, GET /runs/{run_id}/run_steps/{id}, GET /assignments) now surface a blocked_kind string ("stop_task", "approval", or null) instead of the blocked_by_stop_task boolean, naming the kind of step that holds this one; blocking_run_step_id names the step itself.
blocked_kind is non-null exactly when another run step holds this step, and blocking_run_step_id is populated exactly then — no nil-pointer case. "stop_task": an upstream stop task is still incomplete. "approval": the lock chain terminates at an approval whose reviewed work is pending (e.g. substeps under an approval header) — fetch the blocker to see its can_decide / decision_blocked_reason. Under the old boolean these approval-lock cases reported true with a nil pointer.blocked_kind: null — it is not held by another step, and describes its readiness via can_decide: false / decision_blocked_reason: "reviewed_steps_incomplete".blocked_by_stop_task (RunStepApiSerializer) is untouched.GET /runs/{id}, GET /runs/{run_id}/run_steps, and GET /runs/{run_id}/run_steps/{id} now include steps locked by a stop task or by approval readiness (matching what the web UI renders with a lock). Previously main steps were limited to strictly-actionable ones, so e.g. a pending approval gate was invisible — and undecidable-by-discovery — until it unlocked. Locked steps self-describe via blocked_by_stop_task / blocking_run_step_id (and, on approvals, can_decide / decision_blocked_reason); write actions on locked steps still return 422. Hidden (not-applicable) steps remain excluded from lists and still 422 on fetch.include_locked: new query parameter on GET /assignments (default false). The default contract is unchanged — every listed assignment is actionable now. include_locked=true also lists visible-locked assignments (e.g. approvals awaiting reviewed work, composable with kind=approval); assignment objects now include blocked_by_stop_task and blocking_run_step_id so locked entries are self-describing.can_decide now means “a decision call by the authenticated user will succeed right now”: it reflects readiness (reviewed steps complete), whether the approval is already decided, user eligibility, and the four-eyes rule. Previously it reflected eligibility only, so can_decide: true could still yield a 422.decision_blocked_reason on approval steps: why can_decide is false — reviewed_steps_incomplete, already_decided, not_eligible, four_eyes_completer, or step_not_actionable — or null when decidable.POST /runs/{run_id}/run_steps/{id}/decision 422 responses now return a machine-readable reason in error (same vocabulary as decision_blocked_reason) plus a blockers array listing every applicable reason. Previously error was prose (e.g. "Not an approval step").422 already_decided (previously an additional decision was recorded). An approved approval reopens automatically when any reviewed step changes.422 with error: "not_eligible" instead of 403.kind (masked to "standard" without the approvals flag) and, on approval steps, approval_status, reviewed_run_step_ids, can_decide, and decision_blocked_reason. Approval assignments carry a decide action link instead of complete/skip (which are rejected on approval steps while undecided). New kind query parameter on GET /assignments — kind=approval lists approvals awaiting the user’s decision; assignments only include actionable steps, so a listed approval is decidable now._links on an undecided approval step now carry decide (pointing at the decision endpoint) instead of complete/skip, which would 422 there; uncomplete/reset are no longer advertised on decided approvals (reopening happens automatically when reviewed work changes). If an approved approval is reopened without being invalidated (its latest review is still approved), it completes like a plain step and complete/skip are offered again.PATCH /runs/{id} now accepts due_at (preferred) and late_at (deprecated alias) to update the due date on an existing run. Previously only title and participants could be updated via v2; changing a run’s due date required falling back to v1.approvals account flag).
kind — now also "approval". An approval run step gates one or more reviewed steps; it is decided (approved/rejected), not completed.approval_status — "pending", "approved", "rejected", or "stale" (present on approval steps).reviewed_run_step_ids — the run steps this approval gates.under_review / reviewing_run_step_id — on reviewed steps, whether an approval gates them and which one.can_decide — whether the authenticated user may decide this approval.POST /runs/{run_id}/run_steps/{id}/decision with decision (approved/rejected), a required comment, and rejected_run_step_ids (steps to reopen on rejection). The person deciding cannot be a completer of any reviewed step (four-eyes, enforced server-side). Accounts without the approvals flag never see kind: "approval".field_values submitted to POST /runs are now validated against each field’s format (number constraints, list/data-table membership, and other format rules) at create time. A value that fails validation now returns 422 and no run is created. Previously the run was created and the invalid value was silently dropped. Valid values are also now applied to the run’s fields immediately on start rather than shortly after via a background job. Token values ({{...}}) are still resolved at run time and are not validated at create. (The same stricter behavior applies to the v1 API and the web new-run form.)POST/PATCH /workflows/{workflow_id}/steps/{step_id}/data_settings now accept four optional constraint keys in value_format_config when value_format is "number": min, max, max_decimal_places, and allow_negative. Previously these keys were silently dropped.422 with a descriptive error when the value is out of range.step and run_step hookables:
step_started — fires when a step transitions to in-progress.step_assigned — fires when a step gains a new user, role, or external assignee.step_tagged — fires when a run step’s tag list changes; takes a tag_list filter (same shape as tag_changed) so only changes touching one of the listed tags trigger the webhook.POST /webhooks now accepts tag_list when event is step_tagged (in addition to tag_changed). Webhook responses include tag_list for both tag-filtered events.kind — "standard" or "stop_task". A stop_task run step blocks subsequent siblings in the same section until it is completed.blocked_by_stop_task — boolean. true when an upstream stop task in the same section has not yet been completed.blocking_run_step_id — id of the run step currently blocking this one, or null if not blocked.stop_tasks account flag. Accounts without the flag will only ever see kind: "standard" and blocked_by_stop_task: false in responses, since stop tasks cannot be authored without the flag.run.title field in assignment responses now returns the run’s own title (nullable) instead of the combined detailed title. A new run.workflow_title field returns the workflow title separately.title is now documented as nullable, matching actual behavior (runs may have no custom title).GET /runs/{run_id}/run_steps/{run_step_id}/fields/{id} — read a single field.PUT /runs/{run_id}/run_steps/{run_step_id}/fields/{id} — update a single field by field_id.data action on PATCH /runs/{run_id}/run_steps/{id} now returns a 422 for steps with multiple data fields, directing consumers to the per-field endpoint instead.POST /runs/{run_id}/run_steps/{run_step_id}/fields now rejects blank values with an error instead of implicitly uncompleting the step. Use the explicit uncomplete endpoint with clear_data to clear field values.expand_substeps query parameter on GET /runs/{run_id}/run_steps/{id} now works correctly. Previously it was not reaching the serializer, causing paginated (empty) results instead of inline substeps.GET /runs/{run_id}/run_steps/{run_step_id}/fields — returns field-addressed data collection definitions and current values. Each field is keyed by field_id.POST /runs/{run_id}/run_steps/{run_step_id}/fields — batch-set data by field_id (auto-completes on save, uncompletes on clear).late_at is deprecated in favor of due_at across all endpoints. late_at continues to work but new integrations should use due_at.POST /runs and PATCH /runs/{run_id}/run_steps/{id} now accept due_at as the preferred parameter for due dates. When both due_at and late_at are provided, late_at takes precedence for backwards compatibility.POST /runs/{run_id}/run_steps now accepts due_at as an alias for late_at.due_at and late_at fields (identical values).GET /runs now supports due_at_asc and due_at_desc sort options (aliases for late_at_asc/late_at_desc).GET /runs/{id}/run_steps endpoint for listing run steps with filtering (type, status, parent_run_step_id, step_id) and hierarchical mode support.GET /runs/{id}/summary endpoint returning section-level completion stats and overall progress percentage.GET /workflows/{id}/steps endpoint for listing workflow step definitions with filtering and hierarchy support.POST /runs now accepts workflow_id as the preferred parameter for specifying the workflow. checklist_id continues to work but is deprecated.content array to run step and workflow step responses. Each entry includes a type (text, image, file, or video) and the corresponding value or URL. Text content on run steps is returned with dynamic values (e.g. {{run.title}}) resolved.GET /users and GET /users/{id} endpoints for listing and retrieving users (admin-only, supports search, department filtering, and pagination).late_at field on Run responses and fixed its format to ISO 8601 (matches due_at).clear_data option to run step uncomplete action.