# Zuse Bifrost Documentation > Machine-readable index of the Zuse Bifrost documentation site. > Base URL: https://alm-docs.labs.zuselab.dev ## (root) - [Welcome](https://alm-docs.labs.zuselab.dev) — How to plan, build, test, release and maintain a project in Zuse Bifrost, and how to reach it over the API, webhooks and MCP. ## account - [Appearance](https://alm-docs.labs.zuselab.dev/account/appearance) — Pick light mode, dark mode or your device's own setting for the application, on the device you are using. - [Your Account](https://alm-docs.labs.zuselab.dev/account) — Everything about the account you sign in with: how it is created, how you sign in, what protects it, and the preferences that follow you. - [Accepting an Invitation](https://alm-docs.labs.zuselab.dev/account/invitations) — What happens when you follow an invitation link, whether you are new, already have an account, or are signed in as someone else, and why the link is signed. - [Notifications](https://alm-docs.labs.zuselab.dev/account/notifications) — Read every alert in the Notifications Center, mark it read or open it, and choose which classes of alert reach your email and your linked chat. - [Passkeys](https://alm-docs.labs.zuselab.dev/account/passkeys) — Register passkeys to sign in with a fingerprint, a face scan or your device lock, keep one per device, and remove the ones you no longer hold. - [Changing Your Password](https://alm-docs.labs.zuselab.dev/account/password) — Set a new password from Account Settings → Security, what the form asks for, what happens to your other sessions, and how an account that signs in with a passkey alone sets one. - [Profile](https://alm-docs.labs.zuselab.dev/account/profile) — Keep the name, email address and job title on your account accurate, and verify a new address after you change it. - [Browser Sessions](https://alm-docs.labs.zuselab.dev/account/sessions) — See every browser signed in to your account and sign the others out when a device is lost, shared or unfamiliar. - [Signing In](https://alm-docs.labs.zuselab.dev/account/sign-in) — Sign in with your email and password or with a passkey, answer the two-factor challenge, stay signed in, and recover when a password or link no longer works. - [Timezone and Language](https://alm-docs.labs.zuselab.dev/account/timezone-and-locale) — Choose the timezone every date and time in the application is read and shown in, understand how it was detected, and pick the interface language. - [Two-Factor Authentication](https://alm-docs.labs.zuselab.dev/account/two-factor-auth) — Add a six-digit code from an authenticator app to your password sign-in, save your recovery codes, and recover when you lose the device. ## api - [Ability Catalogue](https://alm-docs.labs.zuselab.dev/api/v1/abilities) — Every ability string a token can carry, how the catalogue mirrors the team permissions, what enforces it on the API today, and how to pick the right ones when minting. - [Agent Readiness](https://alm-docs.labs.zuselab.dev/api/v1/agent-readiness) — The documents an agent reads to find Zuse Bifrost on its own: the API catalog, the MCP server card, the agent skills, the OAuth metadata, AGENTS.md and llms.txt, each built from the running application. - [Authentication](https://alm-docs.labs.zuselab.dev/api/v1/authentication) — Personal access tokens, ability scopes and tenant resolution for the REST API and the MCP server, and what the API will not accept. - [Error Codes](https://alm-docs.labs.zuselab.dev/api/v1/errors) — The complete catalogue of error codes from the REST API and the MCP server, their HTTP statuses and what to do about each. - [Idempotency](https://alm-docs.labs.zuselab.dev/api/v1/idempotency) — Every POST, PATCH, PUT and DELETE takes an Idempotency-Key header so a retry never double-creates or double-mutates. - [REST API](https://alm-docs.labs.zuselab.dev/api/v1) — Read and write the project record from your own code, under the same permissions a person has. - [OpenAPI Specification](https://alm-docs.labs.zuselab.dev/api/v1/openapi) — Where the machine-readable OpenAPI document lives, what it carries beyond paths, and how to load it into your tooling. - [Pagination](https://alm-docs.labs.zuselab.dev/api/v1/pagination) — How list endpoints wrap data in a meta and links envelope, the 100-row page cap, and how to iterate a large result set. - [REST API Quickstart](https://alm-docs.labs.zuselab.dev/api/v1/quickstart) — Mint a personal access token, call your first endpoint and receive your first signed webhook, end to end in under five minutes. - [Rate Limiting](https://alm-docs.labs.zuselab.dev/api/v1/rate-limiting) — The named rate-limit buckets applied per token to the REST API and the webhook test endpoint, and the headers that report them. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity) — Every mutation Zuse Bifrost performs, whether from the app, the REST API, an MCP tool call, an accepted Heimdall draft or a scheduled system task, is recorded against its subject in the activity log. - [Read a subject's activity](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity/read-a-subjects-activity) — /v1/activity in the Activity API. - [Alert Destinations API](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations) — An alert destination is a place your alerts go: your email address or the Telegram chat linked to your account. - [List your alert destinations](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations/list-your-alert-destinations) — /v1/me/alert-destinations in the Alert Destinations API. - [Replace your alert destinations](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations/replace-your-alert-destinations) — /v1/me/alert-destinations in the Alert Destinations API. - [Decide an approval](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/decide-an-approval) — /v1/approvals/{approval}/decide in the Approvals API. - [Get an approval request](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/get-an-approval-request) — /v1/approvals/{approval} in the Approvals API. - [Approvals API](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals) — An approval request is one time a planning record was put up for approval: an epic, a story, a requirement, a business rule or a use case, a baseline or a change request, submitted by its author and decided by the approvers the project's policy names for that kind of approval. - [List a project's approval policies](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-a-projects-approval-policies) — /v1/projects/{project}/approval-policies in the Approvals API. - [List a project's approvals](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-a-projects-approvals) — /v1/projects/{project}/approvals in the Approvals API. - [List the inbox](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-the-inbox) — /v1/approvals in the Approvals API. - [Submit a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-baseline) — /v1/baselines/{baseline}/submit in the Approvals API. - [Submit a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-business-rule) — /v1/business-rules/{rule}/submit in the Approvals API. - [Submit a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-change-request) — /v1/change-requests/{changeRequest}/submit in the Approvals API. - [Submit a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-requirement) — /v1/requirements/{requirement}/submit in the Approvals API. - [Submit a story](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-story) — /v1/work-items/{item}/submit in the Approvals API. - [Submit a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-use-case) — /v1/use-cases/{useCase}/submit in the Approvals API. - [Submit an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-an-epic) — /v1/epics/{epic}/submit in the Approvals API. - [Update a project's approval policy](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/update-a-projects-approval-policy) — /v1/projects/{project}/approval-policies in the Approvals API. - [Withdraw an approval](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/withdraw-an-approval) — /v1/approvals/{approval}/withdraw in the Approvals API. - [Delete an attachment](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/delete-an-attachment) — /v1/attachments/{attachment} in the Attachments API. - [Download an attachment](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/download-an-attachment) — /v1/attachments/{attachment}/download in the Attachments API. - [Attachments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments) — An attachment is a file pinned on one artefact of a project: a wireframe on a story, a signed specification on a requirement, a sample export on a data entity, a vendor quote on a decision. - [List an artefact's attachments](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/list-an-artefacts-attachments) — /v1/projects/{project}/attachments in the Attachments API. - [Upload an attachment](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/upload-an-attachment) — /v1/projects/{project}/attachments in the Attachments API. - [Add a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/add-a-baseline) — /v1/projects/{project}/baselines in the Baselines API. - [Delete a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/delete-a-baseline) — /v1/baselines/{baseline} in the Baselines API. - [Get a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-baseline) — /v1/baselines/{baseline} in the Baselines API. - [Get a version of a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-version-of-a-baseline) — /v1/baselines/{baseline}/revisions/{revision} in the Baselines API. - [Baselines API](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines) — A baseline is a named set of approved records frozen together: the scope the client signs before the build starts, what a release ships, what a sprint commits to, or the record as it stood at a milestone. - [List a baseline's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-a-baselines-versions) — /v1/baselines/{baseline}/revisions in the Baselines API. - [List baselines](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-baselines) — /v1/projects/{project}/baselines in the Baselines API. - [Move a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/move-a-baseline) — /v1/baselines/{baseline}/status in the Baselines API. - [Restore a version of a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/restore-a-version-of-a-baseline) — /v1/baselines/{baseline}/revisions/{revision}/restore in the Baselines API. - [Set a baseline's items](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/set-a-baselines-items) — /v1/baselines/{baseline}/items in the Baselines API. - [Update a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/update-a-baseline) — /v1/baselines/{baseline} in the Baselines API. - [Add a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/add-a-business-rule) — /v1/projects/{project}/business-rules in the Business Rules API. - [Delete a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/delete-a-business-rule) — /v1/business-rules/{rule} in the Business Rules API. - [Get a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-business-rule) — /v1/business-rules/{rule} in the Business Rules API. - [Get a version of a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-version-of-a-business-rule) — /v1/business-rules/{rule}/revisions/{revision} in the Business Rules API. - [Business Rules API](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules) — A business rule is a decision of the business the system must honour: a limit it refuses to cross, a fact derived from others, a number worked out from others, a conclusion drawn when conditions hold, an action it takes when they do, or a standing policy. - [List a business rule's quality reviews](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-quality-reviews) — /v1/business-rules/{rule}/quality-reviews in the Business Rules API. - [List a business rule's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-versions) — /v1/business-rules/{rule}/revisions in the Business Rules API. - [List business rules](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-business-rules) — /v1/projects/{project}/business-rules in the Business Rules API. - [Move a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/move-a-business-rule) — /v1/business-rules/{rule}/status in the Business Rules API. - [Restore a version of a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/restore-a-version-of-a-business-rule) — /v1/business-rules/{rule}/revisions/{revision}/restore in the - [Review a business rule's quality](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/review-a-business-rules-quality) — /v1/business-rules/{rule}/quality-reviews in the Business Rules API. - [Update a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/update-a-business-rule) — /v1/business-rules/{rule} in the Business Rules API. - [Analyse a change request's impact](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/analyse-a-change-requests-impact) — /v1/change-requests/{changeRequest}/analyse in the Change Requests API. - [Delete a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/delete-a-change-request) — /v1/change-requests/{changeRequest} in the Change Requests API. - [Get a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-change-request) — /v1/change-requests/{changeRequest} in the Change Requests API. - [Get a version of a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-version-of-a-change-request) — /v1/change-requests/{changeRequest}/revisions/{revision} in the - [Change Requests API](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests) — A change request is a change asked for after the record was agreed: a new requirement, a clarification, a defect in the baseline, a regulatory demand, a client's wish, a piece of technical debt or a scope reduction. - [List a change request's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-a-change-requests-versions) — /v1/change-requests/{changeRequest}/revisions in the Change Requests API. - [List change requests](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-change-requests) — /v1/projects/{project}/change-requests in the Change Requests API. - [Move a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/move-a-change-request) — /v1/change-requests/{changeRequest}/status in the Change Requests API. - [Raise a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/raise-a-change-request) — /v1/projects/{project}/change-requests in the Change Requests API. - [Restore a version of a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/restore-a-version-of-a-change-request) — /v1/change-requests/{changeRequest}/revisions/{revision}/restore in - [Update a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/update-a-change-request) — /v1/change-requests/{changeRequest} in the Change Requests API. - [Verify a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/verify-a-change-request) — /v1/change-requests/{changeRequest}/verify in the Change Requests API. - [Archive a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/archive-a-client) — /v1/clients/{client}/archive in the Clients API. - [Create a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/create-a-client) — /v1/clients in the Clients API. - [Delete a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/delete-a-client) — /v1/clients/{client} in the Clients API. - [Get a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/get-a-client) — /v1/clients/{client} in the Clients API. - [Clients API](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients) — A client is an organisation the team does projects for: its names, its primary contact, where it works from and where the relationship stands. - [List clients](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/list-clients) — /v1/clients in the Clients API. - [Restore a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/restore-a-client) — /v1/clients/{client}/restore in the Clients API. - [Update a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/update-a-client) — /v1/clients/{client} in the Clients API. - [Delete a comment](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/delete-a-comment) — /v1/comments/{comment} in the Comments API. - [Edit a comment](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/edit-a-comment) — /v1/comments/{comment} in the Comments API. - [Comments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments) — A comment is a note on one artefact of a project: a question on a requirement, an answer under it, a finding on a story, a decision taken in the margin of a RAID item. - [List an artefact's comments](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/list-an-artefacts-comments) — /v1/projects/{project}/comments in the Comments API. - [Post a comment](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/post-a-comment) — /v1/projects/{project}/comments in the Comments API. - [Reopen a thread](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/reopen-a-thread) — /v1/comments/{comment}/reopen in the Comments API. - [Resolve a thread](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/resolve-a-thread) — /v1/comments/{comment}/resolve in the Comments API. - [Archive a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/archive-a-component) — /v1/components/{component}/archive in the Components API. - [Create a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/create-a-component) — /v1/projects/{project}/components in the Components API. - [Delete a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/delete-a-component) — /v1/components/{component} in the Components API. - [Get a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/get-a-component) — /v1/components/{component} in the Components API. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components) — A component is one application a project is made of: the web app, the API, a mobile app, a background service, an integration, a database or the platform itself. - [List components](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/list-components) — /v1/projects/{project}/components in the Components API. - [Restore a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/restore-a-component) — /v1/components/{component}/restore in the Components API. - [Update a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/update-a-component) — /v1/components/{component} in the Components API. - [Accept a council finding](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/accept-a-council-finding) — /v1/council-findings/{finding}/accept in the Council API. - [Convene the council](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/convene-the-council) — /v1/projects/{project}/council in the Council API. - [Dismiss a council finding](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/dismiss-a-council-finding) — /v1/council-findings/{finding}/dismiss in the Council API. - [Council API](https://alm-docs.labs.zuselab.dev/api/v1/reference/council) — The council is Heimdall with named voices beside it: Odin for architecture and the data model, Thor for testing, Loki for security, Týr for rigour and compliance, Frigg for planning and risk, Baldur for the people who will use the product and Hel for what fails in production. - [List a project's council sessions](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/list-a-projects-council-sessions) — /v1/projects/{project}/council in the Council API. - [Retrieve a council session](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/retrieve-a-council-session) — /v1/council/{session} in the Council API. - [Create a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/create-a-data-entity) — /v1/projects/{project}/data-entities in the Data Entities API. - [Delete a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/delete-a-data-entity) — /v1/data-entities/{entity} in the Data Entities API. - [Get a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/get-a-data-entity) — /v1/data-entities/{entity} in the Data Entities API. - [Data Entities API](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities) — A data entity is a thing the project keeps records of: a customer, an order, a branch, a shipment. - [List a entity's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-a-entitys-versions) — /v1/data-entities/{entity}/revisions in the Data Entities API. - [List data entities](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-data-entities) — /v1/projects/{project}/data-entities in the Data Entities API. - [Move a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/move-a-data-entity) — /v1/data-entities/{entity}/status in the Data Entities API. - [Read one version of a entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/read-one-version-of-a-entity) — /v1/data-entities/{entity}/revisions/{revision} in the Data Entities API. - [Replace a data entity's fields](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/replace-a-data-entitys-fields) — /v1/data-entities/{entity}/fields in the Data Entities API. - [Restore a version of a entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/restore-a-version-of-a-entity) — /v1/data-entities/{entity}/revisions/{revision}/restore in the Data - [Update a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/update-a-data-entity) — /v1/data-entities/{entity} in the Data Entities API. - [Add a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/add-a-decision) — /v1/projects/{project}/decisions in the Decisions API. - [Delete a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/delete-a-decision) — /v1/decisions/{decision} in the Decisions API. - [Get a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-decision) — /v1/decisions/{decision} in the Decisions API. - [Get a version of a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-version-of-a-decision) — /v1/decisions/{decision}/revisions/{revision} in the Decisions API. - [Decisions API](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions) — A decision is the record of a choice the project made: the context it was taken in, the options weighed with their pros and cons, what was decided, what follows, who decided and when, and the earlier decision it replaces. - [List a decision's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-a-decisions-versions) — /v1/decisions/{decision}/revisions in the Decisions API. - [List decisions](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-decisions) — /v1/projects/{project}/decisions in the Decisions API. - [Move a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/move-a-decision) — /v1/decisions/{decision}/status in the Decisions API. - [Restore a version of a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/restore-a-version-of-a-decision) — /v1/decisions/{decision}/revisions/{revision}/restore in the Decisions API. - [Update a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/update-a-decision) — /v1/decisions/{decision} in the Decisions API. - [Add a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/add-a-design-reference) — /v1/projects/{project}/design-references in the Design References API. - [Delete a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/delete-a-design-reference) — /v1/design-references/{reference} in the Design References API. - [Get a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/get-a-design-reference) — /v1/design-references/{reference} in the Design References API. - [Design References API](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references) — A design reference is a link a project keeps to one of its designs: a Figma file, a wireframe, a prototype, a screenshot, an API contract, a sequence diagram, an entity diagram or a document. - [List design references](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/list-design-references) — /v1/projects/{project}/design-references in the Design References API. - [Update a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/update-a-design-reference) — /v1/design-references/{reference} in the Design References API. - [Create an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/create-an-environment) — /v1/projects/{project}/environments in the Environments API. - [Delete an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/delete-an-environment) — /v1/environments/{environment} in the Environments API. - [Freeze an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/freeze-an-environment) — /v1/environments/{environment}/freeze in the Environments API. - [Get an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/get-an-environment) — /v1/environments/{environment} in the Environments API. - [Environments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments) — An environment is one named place a project's software runs, from a developer's machine to production: its kind, the component it runs (or the whole project), its addresses, the branch deployed there, the autodeploy and protected flags, the health the last check found, the version running there and whether a change freeze is in force. - [List environments](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/list-environments) — /v1/projects/{project}/environments in the Environments API. - [Record an environment's health](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/record-an-environments-health) — /v1/environments/{environment}/health in the Environments API. - [Unfreeze an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/unfreeze-an-environment) — /v1/environments/{environment}/unfreeze in the Environments API. - [Update an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/update-an-environment) — /v1/environments/{environment} in the Environments API. - [Add an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/add-an-epic) — /v1/projects/{project}/epics in the Epics API. - [Delete an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/delete-an-epic) — /v1/epics/{epic} in the Epics API. - [Get a version of an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-a-version-of-an-epic) — /v1/epics/{epic}/revisions/{revision} in the Epics API. - [Get an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-an-epic) — /v1/epics/{epic} in the Epics API. - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) — An epic is a body of work the client recognises: the scope a release is cut from and the parent of every story. - [List an epic's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-an-epics-versions) — /v1/epics/{epic}/revisions in the Epics API. - [List epics](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-epics) — /v1/projects/{project}/epics in the Epics API. - [Move an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/move-an-epic) — /v1/epics/{epic}/status in the Epics API. - [Restore a version of an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/restore-a-version-of-an-epic) — /v1/epics/{epic}/revisions/{revision}/restore in the Epics API. - [Update an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/update-an-epic) — /v1/epics/{epic} in the Epics API. - [Add a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/add-a-glossary-term) — /v1/projects/{project}/glossary in the Glossary API. - [Delete a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/delete-a-glossary-term) — /v1/glossary/{term} in the Glossary API. - [Get a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/get-a-glossary-term) — /v1/glossary/{term} in the Glossary API. - [Glossary API](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary) — A glossary term is a word the project agrees on. - [List a term's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-a-terms-versions) — /v1/glossary/{term}/revisions in the Glossary API. - [List glossary terms](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-glossary-terms) — /v1/projects/{project}/glossary in the Glossary API. - [Move a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/move-a-glossary-term) — /v1/glossary/{term}/status in the Glossary API. - [Read one version of a term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/read-one-version-of-a-term) — /v1/glossary/{term}/revisions/{revision} in the Glossary API. - [Restore a version of a term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/restore-a-version-of-a-term) — /v1/glossary/{term}/revisions/{revision}/restore in the Glossary API. - [Update a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/update-a-glossary-term) — /v1/glossary/{term} in the Glossary API. - [Create a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/create-a-group) — /v1/groups in the Groups API. - [Delete a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/delete-a-group) — /v1/groups/{group} in the Groups API. - [Get a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/get-a-group) — /v1/groups/{group} in the Groups API. - [Groups API](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups) — A group is a named bundle of permissions on a team that several members hold at once: put the three people who run the on-call rota into an incident-commanders group and they share its permissions, take one out and they keep only what their own role gives them. - [List groups](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/list-groups) — /v1/groups in the Groups API. - [Replace a group's members](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/replace-a-groups-members) — /v1/groups/{group}/members in the Groups API. - [Update a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/update-a-group) — /v1/groups/{group} in the Groups API. - [Accept a suggestion](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/accept-a-suggestion) — /v1/suggestions/{suggestion}/accept in the Imports API. - [Discard an import](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/discard-an-import) — /v1/imports/{batch}/discard in the Imports API. - [Get an import](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/get-an-import) — /v1/imports/{batch} in the Imports API. - [Import a brief](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/import-a-brief) — /v1/projects/{project}/imports in the Imports API. - [Imports API](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports) — A brief import hands a client's brief to Heimdall and gets back suggestions: the epics, stories, requirements, business rules, use cases, glossary terms, personas, stakeholders, risks, assumptions and decisions a good analyst would draft from it, each with how sure Heimdall was, why, and the passage of the brief it drew on, plus improvements, the gaps and questions a project of this kind leaves open. - [List a project's imports](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-a-projects-imports) — /v1/projects/{project}/imports in the Imports API. - [List an import's suggestions](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-an-imports-suggestions) — /v1/imports/{batch}/suggestions in the Imports API. - [Merge a suggestion](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/merge-a-suggestion) — /v1/suggestions/{suggestion}/merge in the Imports API. - [Reject a suggestion](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/reject-a-suggestion) — /v1/suggestions/{suggestion}/reject in the Imports API. - [Improvements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/improvements) — Improvements are Heimdall's standing suggestions on a project's own record: the gaps a good analyst fills before anyone builds. - [Sweep a project's record](https://alm-docs.labs.zuselab.dev/api/v1/reference/improvements/sweep-a-projects-record) — /v1/projects/{project}/improvements in the Improvements API. - [API Reference](https://alm-docs.labs.zuselab.dev/api/v1/reference) — Every operation of the REST API, generated from the OpenAPI document and grouped by resource. - [Add a label](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/add-a-label) — /v1/projects/{project}/labels in the Labels API. - [Labels API](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels) — A label is a word a project pins on its work items: payments, tech-debt, client-asked. - [List a project's labels](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/list-a-projects-labels) — /v1/projects/{project}/labels in the Labels API. - [Remove a label](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/remove-a-label) — /v1/labels/{label} in the Labels API. - [Rewrite a label](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/rewrite-a-label) — /v1/labels/{label} in the Labels API. - [Links API](https://alm-docs.labs.zuselab.dev/api/v1/reference/links) — A link joins two artefacts of a project with a kind that says how the source relates to the target: a story implements a requirement, a test case verifies a rule, a bug is fixed by a story, a decision supersedes an older one. - [Link two artefacts](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/link-two-artefacts) — /v1/projects/{project}/links in the Links API. - [List an artefact's links](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/list-an-artefacts-links) — /v1/projects/{project}/links in the Links API. - [Unlink two artefacts](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/unlink-two-artefacts) — /v1/links/{link} in the Links API. - [Get the caller's account](https://alm-docs.labs.zuselab.dev/api/v1/reference/me/get-the-callers-account) — /v1/me in the Me API. - [Me API](https://alm-docs.labs.zuselab.dev/api/v1/reference/me) — GET /v1/me is the first call a client makes. - [Notifications API](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications) — Every alert Zuse Bifrost sends a person (an approval waiting on them, an assignment, a mention, a failing test run, a release approved, an incident opened, a security notice, the Monday digest) also lands in the Notifications Center: the sidebar entry with its unread count and popover, and the page at /notifications. - [List the caller's notifications](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/list-the-callers-notifications) — /v1/me/notifications in the Notifications API. - [Mark a notification read](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-a-notification-read) — /v1/me/notifications/{notification}/read in the Notifications API. - [Mark every notification read](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-every-notification-read) — /v1/me/notifications/read-all in the Notifications API. - [Archive a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/archive-a-persona) — /v1/personas/{persona}/archive in the Personas API. - [Create a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/create-a-persona) — /v1/projects/{project}/personas in the Personas API. - [Delete a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/delete-a-persona) — /v1/personas/{persona} in the Personas API. - [Get a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/get-a-persona) — /v1/personas/{persona} in the Personas API. - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas) — A persona is an archetypal user the project is built for: a named, sketched person with goals, frustrations and habits, so the analyst, the developers and the testers picture the same someone. - [List a persona's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-a-personas-versions) — /v1/personas/{persona}/revisions in the Personas API. - [List personas](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-personas) — /v1/projects/{project}/personas in the Personas API. - [Read one version of a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/read-one-version-of-a-persona) — /v1/personas/{persona}/revisions/{revision} in the Personas API. - [Restore a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/restore-a-persona) — /v1/personas/{persona}/restore in the Personas API. - [Restore a version of a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/restore-a-version-of-a-persona) — /v1/personas/{persona}/revisions/{revision}/restore in the Personas API. - [Update a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/update-a-persona) — /v1/personas/{persona} in the Personas API. - [Check the API is up](https://alm-docs.labs.zuselab.dev/api/v1/reference/ping/check-the-api-is-up) — /v1/ping in the Ping API. - [Ping API](https://alm-docs.labs.zuselab.dev/api/v1/reference/ping) — The one endpoint that needs no token. - [Get a project's settings](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/get-a-projects-settings) — /v1/projects/{project}/settings in the Project Settings API. - [Project Settings API](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings) — A project's settings are its per-project knobs: how long a sprint runs and on which days, which timezone its deadlines are read in, what work is sized in and on which scale, who a new item goes to, whether pull requests move items along and whether checklists gate a sprint or a done, whether a client guest may comment, whether Heimdall assists, where alerts go on Telegram, the board's limits and hidden columns, the business hours SLAs count and whether a production outage opens an incident. - [Update a project's settings](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/update-a-projects-settings) — /v1/projects/{project}/settings in the Project Settings API. - [Archive a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/archive-a-project) — /v1/projects/{project}/archive in the Projects API. - [Create a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/create-a-project) — /v1/projects in the Projects API. - [Delete a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/delete-a-project) — /v1/projects/{project} in the Projects API. - [Get a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/get-a-project) — /v1/projects/{project} in the Projects API. - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects) — A project is one client engagement from discovery to maintenance: the container every component, environment, epic, story, requirement, test, release and incident lives under. - [List projects](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/list-projects) — /v1/projects in the Projects API. - [Restore a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/restore-a-project) — /v1/projects/{project}/restore in the Projects API. - [Update a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/update-a-project) — /v1/projects/{project} in the Projects API. - [Add a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/add-a-raid-item) — /v1/projects/{project}/raid-items in the RAID Log API. - [Delete a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/delete-a-raid-item) — /v1/raid-items/{raidItem} in the RAID Log API. - [Get a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-raid-item) — /v1/raid-items/{raidItem} in the RAID Log API. - [Get a version of a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-version-of-a-raid-item) — /v1/raid-items/{raidItem}/revisions/{revision} in the RAID Log API. - [RAID Log API](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log) — The RAID log is where a project writes down what may go wrong and what it leans on: risks (something that may happen and would hurt, scored by probability and impact, with a mitigation and a contingency), assumptions (something taken as true for the plan to hold, reviewed until it is confirmed or fails), issues (something that has happened and hurts now, scored by impact) and dependencies (something the project needs from outside itself). - [List a RAID item's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-a-raid-items-versions) — /v1/raid-items/{raidItem}/revisions in the RAID Log API. - [List RAID items](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-raid-items) — /v1/projects/{project}/raid-items in the RAID Log API. - [Move a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/move-a-raid-item) — /v1/raid-items/{raidItem}/status in the RAID Log API. - [Restore a version of a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/restore-a-version-of-a-raid-item) — /v1/raid-items/{raidItem}/revisions/{revision}/restore in the RAID Log API. - [Update a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/update-a-raid-item) — /v1/raid-items/{raidItem} in the RAID Log API. - [Delete a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/delete-a-repository) — /v1/repositories/{repository} in the Repositories API. - [Disconnect a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/disconnect-a-repository) — /v1/repositories/{repository}/disconnect in the Repositories API. - [Get a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/get-a-repository) — /v1/repositories/{repository} in the Repositories API. - [Repositories API](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories) — A repository is a Git repository a project's code lives in: its provider, the account that owns it, its name, the branch releases are cut from, whether it is private and which component it builds. - [List repositories](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/list-repositories) — /v1/projects/{project}/repositories in the Repositories API. - [Reconnect a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/reconnect-a-repository) — /v1/repositories/{repository}/reconnect in the Repositories API. - [Register a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/register-a-repository) — /v1/projects/{project}/repositories in the Repositories API. - [Update a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/update-a-repository) — /v1/repositories/{repository} in the Repositories API. - [Add a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/add-a-requirement) — /v1/projects/{project}/requirements in the Requirements API. - [Delete a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/delete-a-requirement) — /v1/requirements/{requirement} in the Requirements API. - [Get a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-requirement) — /v1/requirements/{requirement} in the Requirements API. - [Get a version of a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-version-of-a-requirement) — /v1/requirements/{requirement}/revisions/{revision} in the Requirements API. - [Requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements) — A requirement is one sentence the client and the team agree the system must honour: a functional requirement says what it does for a person (CRM-FR-12), a non-functional one says a quality it keeps while it does it (CRM-NFR-3), with the category the quality belongs to and a measure with the metric and the target. - [List a requirement's quality reviews](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-quality-reviews) — /v1/requirements/{requirement}/quality-reviews in the Requirements API. - [List a requirement's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-versions) — /v1/requirements/{requirement}/revisions in the Requirements API. - [List requirements](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-requirements) — /v1/projects/{project}/requirements in the Requirements API. - [Move a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/move-a-requirement) — /v1/requirements/{requirement}/status in the Requirements API. - [Restore a version of a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/restore-a-version-of-a-requirement) — /v1/requirements/{requirement}/revisions/{revision}/restore in the - [Review a requirement's quality](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/review-a-requirements-quality) — /v1/requirements/{requirement}/quality-reviews in the Requirements API. - [Update a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/update-a-requirement) — /v1/requirements/{requirement} in the Requirements API. - [Research API](https://alm-docs.labs.zuselab.dev/api/v1/reference/research) — Research is Heimdall reading the web for what a project's brief never states: what products of this kind commonly do, the laws, regulations and standards that apply in the client's country and industry, the integrations the client will expect, what the market already offers and the security and accessibility expectations the client will be held to. - [List a project's research suggestions](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research-suggestions) — /v1/projects/{project}/suggestions in the Research API. - [List a project's research](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research) — /v1/projects/{project}/research in the Research API. - [Research a project's domain](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/research-a-projects-domain) — /v1/projects/{project}/research in the Research API. - [Create a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/create-a-role) — /v1/roles in the Roles API. - [Delete a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/delete-a-role) — /v1/roles/{role} in the Roles API. - [Get a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/get-a-role) — /v1/roles/{role} in the Roles API. - [Roles API](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles) — A role is the permission set a member holds on a team. - [List roles](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/list-roles) — /v1/roles in the Roles API. - [Update a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/update-a-role) — /v1/roles/{role} in the Roles API. - [Add a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/add-a-stakeholder) — /v1/projects/{project}/stakeholders in the Stakeholders API. - [Get a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/get-a-stakeholder) — /v1/stakeholders/{stakeholder} in the Stakeholders API. - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders) — A stakeholder is a person the project answers to: the client's sponsor and product owner, the people who sign off requirements, UAT runs and releases, Zuse's own account manager or architect, and third parties such as an auditor or a vendor. - [List a stakeholder's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-a-stakeholders-versions) — /v1/stakeholders/{stakeholder}/revisions in the Stakeholders API. - [List stakeholders](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-stakeholders) — /v1/projects/{project}/stakeholders in the Stakeholders API. - [Read one version of a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/read-one-version-of-a-stakeholder) — /v1/stakeholders/{stakeholder}/revisions/{revision} in the Stakeholders API. - [Remove a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/remove-a-stakeholder) — /v1/stakeholders/{stakeholder} in the Stakeholders API. - [Restore a version of a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/restore-a-version-of-a-stakeholder) — /v1/stakeholders/{stakeholder}/revisions/{revision}/restore in the - [Update a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/update-a-stakeholder) — /v1/stakeholders/{stakeholder} in the Stakeholders API. - [Change a team member's role](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/change-a-team-members-role) — /v1/teams/{team}/members/{member}/role in the Team Members API. - [Get a team member](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/get-a-team-member) — /v1/teams/{team}/members/{member} in the Team Members API. - [Team Members API](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members) — Team members are the people inside a team: the owner and everyone invited to work in it, staff and client guests alike. - [Invite a team member](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/invite-a-team-member) — /v1/teams/{team}/members in the Team Members API. - [List a team's members](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/list-a-teams-members) — /v1/teams/{team}/members in the Team Members API. - [List a team's pending invitations](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/list-a-teams-pending-invitations) — /v1/teams/{team}/invitations in the Team Members API. - [Remove a team member](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/remove-a-team-member) — /v1/teams/{team}/members/{member} in the Team Members API. - [Withdraw an invitation](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/withdraw-an-invitation) — /v1/teams/{team}/invitations/{invitation} in the Team Members API. - [Create a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/create-a-team) — /v1/teams in the Teams API. - [Delete a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/delete-a-team) — /v1/teams/{team} in the Teams API. - [Get a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-a-team) — /v1/teams/{team} in the Teams API. - [Get the current team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-the-current-team) — /v1/teams/current in the Teams API. - [Teams API](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams) — A team is the tenant: the container every project, role, group, token and webhook endpoint hangs off. - [List teams](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/list-teams) — /v1/teams in the Teams API. - [Rename a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/rename-a-team) — /v1/teams/{team} in the Teams API. - [Get a token](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/get-a-token) — /v1/tokens/{token} in the Tokens API. - [Tokens API](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens) — Personal access tokens are what authorise every REST and MCP call. - [List the caller's tokens](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/list-the-callers-tokens) — /v1/tokens in the Tokens API. - [Mint a token](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/mint-a-token) — /v1/tokens in the Tokens API. - [Revoke a token](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/revoke-a-token) — /v1/tokens/{token} in the Tokens API. - [Add a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/add-a-use-case) — /v1/projects/{project}/use-cases in the Use Cases API. - [Delete a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/delete-a-use-case) — /v1/use-cases/{useCase} in the Use Cases API. - [Get a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-use-case) — /v1/use-cases/{useCase} in the Use Cases API. - [Get a version of a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-version-of-a-use-case) — /v1/use-cases/{useCase}/revisions/{revision} in the Use Cases API. - [Use Cases API](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases) — A use case is how an actor reaches a goal with the system, step by step: the persona who acts, the other actors in the room, the goal, what triggers it, what must be true before and after, how often it happens, and the steps (what the actor does and what the system does in reply) in a main flow with alternate and exception flows branching off numbered main steps. - [List a use case's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-a-use-cases-versions) — /v1/use-cases/{useCase}/revisions in the Use Cases API. - [List use cases](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-use-cases) — /v1/projects/{project}/use-cases in the Use Cases API. - [Move a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/move-a-use-case) — /v1/use-cases/{useCase}/status in the Use Cases API. - [Replace a use case's steps](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/replace-a-use-cases-steps) — /v1/use-cases/{useCase}/steps in the Use Cases API. - [Restore a version of a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/restore-a-version-of-a-use-case) — /v1/use-cases/{useCase}/revisions/{revision}/restore in the Use Cases API. - [Update a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/update-a-use-case) — /v1/use-cases/{useCase} in the Use Cases API. - [Follow an artefact](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/follow-an-artefact) — /v1/projects/{project}/watchers in the Watchers API. - [Watchers API](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers) — A watcher is a person following one artefact: they are told of every comment posted on it. - [List an artefact's followers](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/list-an-artefacts-followers) — /v1/projects/{project}/watchers in the Watchers API. - [Stop following](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/stop-following) — /v1/watchers/{watcher} in the Watchers API. - [Get a webhook delivery](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/get-a-webhook-delivery) — /v1/webhook-deliveries/{delivery} in the Webhook Deliveries API. - [Webhook Deliveries API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries) — Every event Zuse Bifrost posts to one of your webhook endpoints is a delivery: one row per endpoint per event, carrying the signed envelope that was sent, the target's response and where the row is on the retry ladder. - [List webhook deliveries](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/list-webhook-deliveries) — /v1/webhook-deliveries in the Webhook Deliveries API. - [Retry a webhook delivery](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-a-webhook-delivery) — /v1/webhook-deliveries/{delivery}/retry in the Webhook Deliveries API. - [Retry every recent dead delivery](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-every-recent-dead-delivery) — /v1/webhook-deliveries/retry-dead in the Webhook Deliveries API. - [Delete a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/delete-a-webhook-endpoint) — /v1/webhook-endpoints/{endpoint} in the Webhook Endpoints API. - [Get a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/get-a-webhook-endpoint) — /v1/webhook-endpoints/{endpoint} in the Webhook Endpoints API. - [Webhook Endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) — A webhook endpoint is a URL of yours that Zuse Bifrost posts events to, with the events it subscribes to and a signing secret you verify each delivery against. - [List webhook endpoints](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/list-webhook-endpoints) — /v1/webhook-endpoints in the Webhook Endpoints API. - [Pause a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/pause-a-webhook-endpoint) — /v1/webhook-endpoints/{endpoint}/pause in the Webhook Endpoints API. - [Register a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/register-a-webhook-endpoint) — /v1/webhook-endpoints in the Webhook Endpoints API. - [Resume a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/resume-a-webhook-endpoint) — /v1/webhook-endpoints/{endpoint}/resume in the Webhook Endpoints API. - [Rotate a webhook endpoint's secret](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/rotate-a-webhook-endpoints-secret) — /v1/webhook-endpoints/{endpoint}/rotate-secret in the Webhook Endpoints API. - [Test-fire a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/test-fire-a-webhook-endpoint) — /v1/webhook-endpoints/{endpoint}/test in the Webhook Endpoints API. - [Webhook Events API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-events) — A read-only polling endpoint that surfaces the most recent webhook deliveries for a given event type. - [Poll recent webhook events](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-events/poll-recent-webhook-events) — /v1/webhook-events in the Webhook Events API. - [Add a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/add-a-work-item) — /v1/projects/{project}/work-items in the Work Items API. - [Assign a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/assign-a-work-item) — /v1/work-items/{item}/assign in the Work Items API. - [Delete a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/delete-a-work-item) — /v1/work-items/{item} in the Work Items API. - [Get a version of a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-version-of-a-work-item) — /v1/work-items/{item}/revisions/{revision} in the Work Items API. - [Get a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-work-item) — /v1/work-items/{item} in the Work Items API. - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) — A work item is one row of the tracker: a story, a task, a sub-task, a bug, a spike or an improvement. - [List a work item's quality reviews](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-quality-reviews) — /v1/work-items/{item}/quality-reviews in the Work Items API. - [List a work item's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-versions) — /v1/work-items/{item}/revisions in the Work Items API. - [List my work items](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-my-work-items) — /v1/me/work-items in the Work Items API. - [List work items](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-work-items) — /v1/projects/{project}/work-items in the Work Items API. - [Move a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/move-a-work-item) — /v1/work-items/{item}/status in the Work Items API. - [Replace a work item's acceptance criteria](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/replace-a-work-items-acceptance-criteria) — /v1/work-items/{item}/acceptance-criteria in the Work Items API. - [Restore a version of a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/restore-a-version-of-a-work-item) — /v1/work-items/{item}/revisions/{revision}/restore in the Work Items API. - [Review a work item's quality](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/review-a-work-items-quality) — /v1/work-items/{item}/quality-reviews in the Work Items API. - [Update a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/update-a-work-item) — /v1/work-items/{item} in the Work Items API. - [Tenancy & Scopes](https://alm-docs.labs.zuselab.dev/api/v1/tenancy-and-scopes) — Every read and write is scoped to one team, scope entries on a token narrow it further to chosen projects, and a guest's token never reaches beyond their invitations. - [Versioning & Deprecation](https://alm-docs.labs.zuselab.dev/api/v1/versioning-and-deprecation) — How the API is versioned under /v1, how new functionality is added, and how breaking changes are announced and retired. ## building - [Board](https://alm-docs.labs.zuselab.dev/building/board) — The Board page of a project: the running sprint's work in a column per status, dragged along its lifecycle and into its place in the order, with the limits each column keeps, swimlanes by epic, person or kind, and the quick filters. - [Building](https://alm-docs.labs.zuselab.dev/building) — Where the team turns the planning record into working software: the sprints it plans and commits to, the board it works them on, with the time tracking, the definitions of ready and done and the Git links joining as they land. - [Sprints](https://alm-docs.labs.zuselab.dev/building/sprints) — Plan a sprint with its dates, goal and capacity, pull its scope from the backlog, put it to the team for the commitment that starts it, follow it while it runs, close it with its velocity and carry the unfinished work on. ## copilot - [Brief import](https://alm-docs.labs.zuselab.dev/copilot/brief-import) — What Heimdall is given before it reads a brief, how it fills each kind of suggestion from one uniform row, how it judges its own confidence, why it runs on the queue as you, what it costs and where it stops. - [The Council](https://alm-docs.labs.zuselab.dev/copilot/council) — Heimdall's council: seven named voices, each a lens, convened on a story, a requirement, a rule, a brief import or a project Heimdall scaffolded in conversation; how the voices research the ground, read the record, answer one another and refine the plan Heimdall drafts, how Heimdall rules, what accepting a finding writes, what a sitting costs and where it stops. - [Impact analysis](https://alm-docs.labs.zuselab.dev/copilot/impact-analysis) — What Heimdall reads before it weighs a change request, what it is told to look for, how its answer becomes the draft and the numbers on the request, what a reading costs, how the council's change impact moment differs, and where it stops. - [Improvement Sweep](https://alm-docs.labs.zuselab.dev/copilot/improvement-sweep) — The five gaps Heimdall's sweep looks for in a project's record, how it proposes a card per gap on the review queue, why the same idea is never proposed twice, when it runs on its own, what accepting a card does and what it costs: nothing. - [Heimdall](https://alm-docs.labs.zuselab.dev/copilot) — The operator inside Zuse Bifrost: ask it about your projects, teams, webhooks and history in plain words, have it do the routine work, and confirm anything that changes what already exists. Beside every page, metered against the team's monthly AI budget. - [Inline assists](https://alm-docs.labs.zuselab.dev/copilot/inline-assist) — Ask Heimdall beside any long field of an epic, story, requirement, rule, use case, RAID item, decision or dictionary entry: draft it, improve it, shorten it, make it testable or rewrite it as Given, When, Then, then take the draft or drop it. - [The Panel](https://alm-docs.labs.zuselab.dev/copilot/panel) — Heimdall sheet beside every page, end to end: opening it, the context chip and the eye, the start screen, threads, the composer, the model tiers and the review switch, the work timeline and the streamed answer, buttons and follow-ups, confirmation cards and the budget line. - [Quality review](https://alm-docs.labs.zuselab.dev/copilot/quality-review) — What Heimdall reads before it judges a record, how it grades and codes a finding, why the score is arithmetic, what a review costs and where it stops: it never writes to the record. - [Research](https://alm-docs.labs.zuselab.dev/copilot/research) — What Heimdall is told before it reads the web for a project, what it searches for, how its notes and suggestions are shaped and filtered, why a dismissed idea never returns, what a run costs and where it stops: it never writes to the record. - [Permissions & Safety](https://alm-docs.labs.zuselab.dev/copilot/safety-and-permissions) — Who may use Heimdall, how a team member's role bounds what it can do, the confirmation gate on anything that changes what exists, what the activity log records, what the model is shown and what it is never shown, and the models behind it. - [Start a Project in Conversation](https://alm-docs.labs.zuselab.dev/copilot/start-a-project) — Tell Heimdall about a client and what they need; it creates the client and the project, writes the record as drafts, asks only for what it cannot infer, and convenes the council on the result inside the conversation. - [Usage & Budgets](https://alm-docs.labs.zuselab.dev/copilot/usage-and-budgets) — How every model call is priced and filed, what a turn costs on each tier, the team's monthly AI budget with its warning line and its cap, who is told when a line is crossed, what is never metered, and where to read the statement. ## documents - [Business Requirements Document](https://alm-docs.labs.zuselab.dev/documents/brd) — What the BRD a project generates prints, section by section, where each section is read from, what the executive summary and the sign-off page ask of the client, and how to render the copy the sponsor signs. - [Generated Documents](https://alm-docs.labs.zuselab.dev/documents/generated-documents) — Generate a business requirements document or a software requirements specification from the project's own record, edit the opening page Heimdall drafts, render it again, put it to the project manager, and share the final copy with the client. - [Documents](https://alm-docs.labs.zuselab.dev/documents) — The documents a project generates from the record its team wrote once: the business requirements document the client signs and the software requirements specification the team builds against, each rendered, reviewed, approved and shared from one page. - [Software Requirements Specification](https://alm-docs.labs.zuselab.dev/documents/srs) — What the SRS a project generates prints, section by section, where each section is read from, how the introduction is drafted, and how the team builds and tests against the final copy. ## guests - [Approving as a Client](https://alm-docs.labs.zuselab.dev/guests/approving-requirements) — What an approval that names you looks like, how to approve, request changes or reject it with a reason, what your verdict does, and what the team sees afterwards. - [Documents Shared With You](https://alm-docs.labs.zuselab.dev/guests/documents-shared-with-you) — Where the final BRD and SRS of your projects appear, how to download or open one, what the status badges mean, and why a draft the team is still working on is not shown to you. - [For Clients](https://alm-docs.labs.zuselab.dev/guests) — The guest's view of a project: the approved record, the approvals assigned to you, and what stays on the team's side of the line. - [Signing Off the Scope](https://alm-docs.labs.zuselab.dev/guests/signing-off-scope) — What a frozen baseline is, how the sign-off reaches you and what your signature means, what a change request is and how to raise one, and what you read of each afterwards. ## integrations - [Integrations](https://alm-docs.labs.zuselab.dev/integrations) — GitHub, CI/CD pipelines, Telegram, email, Postman and custom agents. - [Telegram](https://alm-docs.labs.zuselab.dev/integrations/telegram) — Link your account to the Zuse Bifrost bot with a one-time code, receive alerts in a private chat, and ask the bot who you are linked as. ## introduction - [Introduction](https://alm-docs.labs.zuselab.dev/introduction) — The five phases of a project in Zuse Bifrost, the one record they share, and the roles that work in it. ## mcp - [MCP Authentication](https://alm-docs.labs.zuselab.dev/mcp/v1/authentication) — How a client connects to the MCP server, by signing in with OAuth 2.1 or with a personal access token, and how every tool enforces its ability. - [Connecting ChatGPT Desktop](https://alm-docs.labs.zuselab.dev/mcp/v1/connecting-chatgpt-desktop) — Configure ChatGPT's developer-mode connectors to work with Zuse Bifrost, by signing in or with a personal access token. - [Connecting Claude](https://alm-docs.labs.zuselab.dev/mcp/v1/connecting-claude-desktop) — Add the Zuse Bifrost MCP server to claude.ai, Claude Desktop and Claude Code as a connector, by signing in or with a personal access token. - [Connecting Cursor](https://alm-docs.labs.zuselab.dev/mcp/v1/connecting-cursor) — Configure Cursor's MCP support to use the Zuse Bifrost server, by signing in or with a personal access token. - [Connecting VS Code](https://alm-docs.labs.zuselab.dev/mcp/v1/connecting-vscode-claude) — Add the Zuse Bifrost MCP server to VS Code or the Claude Code extension, by signing in or with a personal access token. - [MCP Server](https://alm-docs.labs.zuselab.dev/mcp/v1) — Connect Claude, Cursor, ChatGPT or VS Code to a project's record over the Model Context Protocol and let an agent read context and write the record back, as far as the person's role allows. - [Prompting Guide](https://alm-docs.labs.zuselab.dev/mcp/v1/prompting-guide) — Prompt patterns that respect rate limits, keep the blast radius small and keep the person in view when an LLM drives the MCP server. - [MCP Quickstart](https://alm-docs.labs.zuselab.dev/mcp/v1/quickstart) — Add the Zuse Bifrost MCP server as a Claude connector, sign in, approve the connection and run your first tool call. - [Resources Reference](https://alm-docs.labs.zuselab.dev/mcp/v1/resources-reference) — The three read-only JSON resources the MCP server publishes so an agent can ground itself before it calls a tool. - [MCP Security](https://alm-docs.labs.zuselab.dev/mcp/v1/security) — How OAuth connections and personal access tokens are scoped, revoked and audited, and what to do the moment a credential leaks. - [Tools Reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools-reference) — Every MCP tool the Zuse Bifrost server exposes today, grouped by what it works on, with the ability each one enforces. - [get_me](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/get-me) — Describe the person the token acts as — the team the token is scoped to, every team held, where alerts go, the unread count and whether Telegram is linked. Takes no input. - [Account Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account) — The account behind the token: who is acting, which team the token is scoped to, where alerts go and what sits unread in the Notifications Center. - [list_notifications](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/list-notifications) — Read the Notifications Center of the person the token acts for — every alert sent to them, newest first, with its class, where it points and whether it was read. - [mark_all_notifications_read](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-all-notifications-read) — Mark every unread entry of the person's Notifications Center as read in one call. - [mark_notification_read](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-notification-read) — Mark one entry of the person's Notifications Center as read. - [assign_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/assign-work-item) — Hand a work item to a member of the team, or to nobody, and announce the hand-over. - [create_label](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-label) — Adds a word the project pins on its work items, with a colour and a meaning. - [create_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-work-item) — Adds a work item of any kind to a project's backlog, with everything set at once; the full form beside create_story_draft. - [delete_label](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/delete-label) — Deletes a label of a project after the person confirms; every item wearing it loses it. - [delete_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/delete-work-item) — Deletes a work item after the person confirms; cancelling with a reason keeps it on the record instead. - [get_board_snapshot](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-board-snapshot) — Reads a project's board as the Board page draws it, the columns the project keeps in order with their limits, tallies and cards, for the running sprint, a sprint by id, every open item or the unplanned ones; a guest token reads none. - [get_sprint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-sprint) — Fetches one sprint by id with everything the list gives and its scope, the work items it holds in the backlog's order with their status and points, plus the points planned and done. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — One work item by id, with its reference, kind, title, prose, status, priority, people, estimates, the fields its kind adds, the components it touches and its acceptance criteria in order. - [Build Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build) — The tracker of a project: its stories, tasks, sub-tasks, bugs, spikes and improvements, each numbered from the same sequence as the epics (`CRM-142`) with its kind, status from the backlog to done, resolution, priority, the people on it, its estimates and dates, the fields its kind adds and, for a story, the acceptance criteria that say when it is done. - [list_labels](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-labels) — A project's labels by name, each with its colour, what it means and how many work items carry it; the vocabulary to read before you draft or filter stories. - [list_my_work](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-my-work) — The work items assigned to the person the token acts as, across every project the token may see, open ones by default. - [list_sprints](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-sprints) — Lists a project's sprints in number order with their dates, status, capacity, the points committed to and finished, the velocity, the item count and who started and closed them, narrowed by status, open or words; a guest token reads none. - [list_work_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items) — A project's stories, tasks, sub-tasks, bugs, spikes and improvements in the project's order, with reference, kind, status, priority, people and estimate, narrowed by the typed filters and for a guest token. - [move_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — Move a work item along its lifecycle, from the backlog through ready, in progress, in review and in QA to done or cancelled, with a resolution on a close. - [update_label](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-label) — Changes a label in part; every item wearing it shows the new word at once. - [update_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-work-item) — Changes a work item in part; the criteria, components and labels are replaced only when their list is sent, and every change writes a version. - [create_repository](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/create-repository) — Registers where a component's code lives, so the Git integration can link branches, commits and pull requests to it later. - [delete_repository](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/delete-repository) — Removes a repository from a project's registry after the person confirms; the code itself is untouched. - [get_repository](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/get-repository) — One repository by id, with its provider, owner, name, default branch, address, privacy, component and disconnected stamp. - [Code Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code) — Where a project's code lives: the Git repositories the team registered, each with its provider, the owner and name as the provider spells them, the branch releases are cut from, the address, whether it is private and the component it builds. - [list_repositories](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/list-repositories) — Paginated list of the Git repositories a project's code lives in, each with its provider, owner, name, default branch, privacy and component, narrowed by provider, component, disconnected state or search words. - [update_repository](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/update-repository) — Changes a registered repository in part. - [create_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/create-data-entity) — Adds a thing the system keeps records of, with its fields, so the data dictionary a brief implies is laid down as the conversation goes. - [create_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/create-glossary-term) — Adds a word the project agrees on, so the vocabulary a brief uses is pinned as the conversation goes. - [delete_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-data-entity) — Deletes a data entity and its fields after the person confirms; deprecating keeps it on the record instead. - [delete_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-glossary-term) — Deletes a glossary term after the person confirms; deprecating keeps the word on the record instead. - [get_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-data-entity) — One data entity by id with its fields in order, each with its type, flags, validation, example and description. - [get_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-glossary-term) — One glossary term by id, with its definition, other names, related terms, source and review status. - [Dictionary Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary) — The dictionary of a project: the glossary (the words the project agrees on, each with one definition, the other names it goes by, the terms it leans on, its source and its review status) and the data entities (the things the project keeps records of, each with its fields, their types, flags, validation and examples, its component, its owner, whether it holds personal data and how long it is kept). - [list_data_entities](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-data-entities) — Paginated list of the things a project keeps records of, each with its component, owner, personal-data flag, retention note, review status and field count, narrowed by status, component, personal data or search words. - [list_glossary_terms](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-glossary-terms) — Paginated list of the words a project agrees on, each with its definition, other names, related terms, source and review status, narrowed by status or search words. - [update_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-data-entity) — Changes a data entity in part, replaces its fields when a list is sent and moves it along its review when a status is sent. - [update_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-glossary-term) — Changes a glossary term in part and moves it along its review when a status is sent. - [generate_document](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/generate-document) — Asks a project for a business requirements document or a software requirements specification as the token's holder, with a title, a file format and a scope, and answers with the document while it renders in the background. - [get_document](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/get-document) — Fetches one generated document by id with its kind, reference, title, format, status, scope, opening prose, stamps and download address, and on request the rendered document as plain words. - [Documents Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents) — The documents a project writes from its own record: the business requirements document the client signs and the software requirements specification the team builds against, each asked for with a kind, a title, a file format (PDF or HTML) and a scope (the record as it stands, with or without drafts, or a frozen baseline), rendered in the background as the person who asked and under their permissions, numbered within its kind (`BRD #2`), with an opening page Heimdall drafts when nobody wrote it, moved to final by the project manager's approval, which supersedes the kind's earlier final copy, and shared with the client's guests from that moment. - [list_documents](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/list-documents) — Lists a project's generated documents newest first with their kind, reference, title, format, status, scope, stamps and download address, narrowed by kind, status or words; a guest token sees the final copies only. - [create_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/create-webhook-endpoint) — Register an outbound webhook endpoint for the token's team. The signing secret is returned in this response only. - [delete_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/delete-webhook-endpoint) — Remove an outbound webhook endpoint. Destructive — an integration listening on it goes dark at once; its delivery log is kept. - [get_activity_log](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-activity-log) — Read the activity log of one subject, newest first — every change made to it, who made it, the surface it came through and the columns that changed. - [get_webhook_delivery](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-delivery) — One outbound webhook delivery by UUID — its event, status, attempts, the payload that was posted and what the endpoint answered. - [get_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-endpoint) — One of the team's outbound webhook endpoints by UUID, with its health counters. The signing secret is never returned. - [Observability Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability) — Where to look when something did not happen: outbound webhook endpoints and their deliveries, and the activity log of every change made to a subject and who made it, from the application, the REST API, an MCP tool, an accepted Heimdall draft or a scheduled job. - [list_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries) — The team's outbound webhook delivery log, newest first — what was posted, what the endpoint answered, and where each row is on the retry ladder. - [list_webhook_endpoints](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-endpoints) — List the outbound webhook endpoints registered for the token's team, paginated, with their health counters. Secrets are never returned. - [pause_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/pause-webhook-endpoint) — Stop deliveries to an endpoint without removing it. Events raised while paused are not queued for it. - [resume_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/resume-webhook-endpoint) — Start deliveries to a paused endpoint again and clear its failure streak. Events raised while paused are not replayed. - [retry_dead_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries) — Replay every webhook delivery the team dead-lettered since an instant, once the consumer is fixed. Defaults to the last 24 hours. - [retry_webhook_delivery](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-webhook-delivery) — Replay one failed or dead-lettered webhook delivery from the start of the retry ladder. A pending or delivered row is refused. - [rotate_webhook_endpoint_secret](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/rotate-webhook-endpoint-secret) — Replace an endpoint's signing secret. The new secret is returned in this response only and the old one stops verifying at once. - [test_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/test-webhook-endpoint) — Queue a synthetic test event to an endpoint so its consumer can be checked end to end. Returns the queued delivery to poll. - [create_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/create-persona) — Adds one of the people the product is built for, so the stories that follow have someone to be written as. - [create_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/create-stakeholder) — Adds one of the people a project answers to, so the sponsor and the product owner a brief names take their seats as the conversation goes. - [delete_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-persona) — Deletes a persona of a project after the person confirms; the stories and use cases written as them keep their words. - [delete_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-stakeholder) — Deletes a stakeholder of a project after the person confirms; approvals that name the seat lose an approver. - [get_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-persona) — Fetch a single persona by UUID — their sketch in full, with goals, frustrations, behaviours, proficiency, devices, quote and archive stamp. - [get_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-stakeholder) — Fetch a single stakeholder by UUID — their side, role, weight, RACI default, approver flags and, for a staff token, contact details, notes and linked account. - [People Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people) — The people of a project: the stakeholders it answers to (the client's sponsor, product owner and approvers, Zuse's own leads, third parties, each with a side, a role, a weight and the approvals that name them) and the personas it is built for (named, sketched users with goals, frustrations and habits, the \"as a …\" of every story). - [list_personas](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas) — Paginated list of the archetypal users a project is built for, with their goals, frustrations, habits, proficiency and devices, narrowed by search words or archived state. - [list_stakeholders](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders) — Paginated list of the people a project answers to, with their side, role, weight and the approvals that name them, narrowed by side, role, approval or search words. - [update_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-persona) — Changes a persona of a project in part; the devices replace the list when sent. - [update_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-stakeholder) — Changes a stakeholder of a project in part, so an approval flag or an email can be added as the person mentions it. - [analyse_change_impact](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/analyse-change-impact) — Asks Heimdall to read one change request against its baseline and the records it names, writes the draft onto the request, fills the impact fields the analyst left empty, and answers with the request; one metered call under the team's AI budget. - [comment_on_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/comment-on-item) — Post a comment on one artefact as the person the token acts as, in Markdown, under a first comment when answering; the people named and the followers are told and comment.created goes out. - [convene_council](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — Convenes Heimdall's council on a brief import, a work item, a requirement, a business rule or a change request with the voices named, and answers with the sitting queued; spends the team's AI budget within the council's share. - [create_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-baseline) — Composes a draft baseline on a project from approved records, with its name, kind and words, and answers with the draft and its record rows; nothing is frozen until a person submits it and the project manager approves. - [create_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-epic) — Adds an epic to a project as a draft, so Heimdall can split a brief into bodies of work and hang the stories under them. - [create_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-raid-item) — Adds any entry of the RAID log, an assumption, an issue or a dependency as well as a risk; the full form beside raise_risk. - [create_requirement_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-requirement-draft) — Add a functional or non-functional requirement to a project as a draft a person reviews, with its statement, source, priority, measure and verification method. - [create_story_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-story-draft) — Add a story to a project's backlog with its sentence, its epic, its priority and its acceptance criteria, as a draft a person reviews. - [create_use_case](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-use-case) — Adds a use case as a draft, with its main, alternate and exception flows, so how a persona reaches a goal is written step by step. - [decide_council_finding](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-council-finding) — Decides one finding of a council sitting on the person's word, after they confirm; accepting takes it up, dismissing sets it aside for good. - [decide_suggestion](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-suggestion) — Decides one row of the review queue on the person's word, after they confirm; accepting writes the record, rejecting settles the row. - [delete_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-baseline) — Deletes a draft baseline with its record rows and versions; a baseline past its draft is the record and is superseded rather than deleted. - [delete_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-business-rule) — Deletes a business rule after the person confirms; deprecating keeps it on the record instead. - [delete_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-change-request) — Deletes a change request that was never agreed, with its links and versions; a request past its gate is closed rather than deleted, and its number is never handed out again. - [delete_decision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-decision) — Deletes a decision after the person confirms; rejecting or deprecating keeps it on the record instead. - [delete_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-epic) — Deletes an epic after the person confirms; the work items under it stay without an epic. - [delete_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-raid-item) — Deletes a RAID item after the person confirms; closing it keeps it on the log instead. - [delete_requirement](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-requirement) — Deletes a requirement after the person confirms; deprecating keeps it on the record instead. - [delete_use_case](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-use-case) — Deletes a use case and its steps after the person confirms; deprecating keeps it on the record instead. - [get_approval_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-approval-request) — Fetches one approval request by UUID with the record it is about, the approvers the policy named, the tally, the clocks and every verdict given, round by round. - [get_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-baseline) — Fetches one baseline by id with the records it holds, each by family, key, title, the version pinned and whether it is still locked or was unlocked by a change request. - [get_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-business-rule) — One business rule by id, with its reference, name, kind, statement, condition and action, exceptions, worked examples, authority, priority, dates in force, status and owner. - [get_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-change-request) — Fetches one change request by id with the records it touches, its impact, Heimdall's draft, its baseline and the people behind it; a guest token reads it without the effort, the cost and the draft. - [get_council_session](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-council-session) — One sitting of Heimdall's council with everything it holds, the verdict, the voices' turns and every finding with what a person decided; free to call. - [get_decision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-decision) — One decision by id, with its reference, kind, title, context, the options weighed, what was decided, consequences, status, the day decided, deciders and the decision it supersedes. - [get_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-epic) — One epic by id, with its reference, goal, prose, owner, priority, status, dates and colour. - [get_import_batch](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-import-batch) — Fetches one brief import with its words and every suggestion Heimdall drafted from it, each with its decision so far. - [get_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-raid-item) — One RAID item by id, with its reference, kind, title, description, area, probability, impact and score, mitigation and contingency, owner and raiser, dates, dependency and status. - [get_requirement](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-requirement) — One requirement by id, with its reference, kind, category, statement, rationale, source, priority, measure, verification method, status and owner. - [get_revision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-revision) — One version of an artefact by id, with its snapshot, the diff from the version before, who wrote it and why. - [get_use_case](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-use-case) — One use case by id, with its reference, title, actor, other actors, goal, trigger, conditions, frequency, priority, status and its steps in order, each in the main flow or in an alternate or exception flow that leaves a main step. - [Planning Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning) — The planning record of a project: its epics (the bodies of work the client recognises, each numbered from the project's tracker sequence, `CRM-12`, with its goal, value, measures, owner, MoSCoW priority, status from draft to done and dates) and its requirements (what the system shall do, functional, `CRM-FR-12`, and the qualities it shall keep, non-functional, `CRM-NFR-3`, each with a category and a measure, with the statement, the source, the priority, the verification method and the status from draft to baselined). - [link_artefacts](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/link-artefacts) — Join two artefacts of a project with a kind read from the source, such as a story that implements a requirement; a repeat, a self-link or a foreign end is refused, and link.created goes out. - [list_attachments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-attachments) — The files pinned on one artefact, newest first, each with its name, size, kind, uploader and the REST endpoint that streams its bytes; the agreed record only for a guest token. - [list_baselines](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-baselines) — Lists a project's baselines in the project's order with their kind, status, version, clocks and record count, narrowed by kind, status or words; a guest token sees the frozen baselines only. - [list_business_rules](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-business-rules) — A project's business rules in the project's order, with reference, name, kind, statement, condition and action, examples, authority, priority, dates in force, status and owner, narrowed for a guest token. - [list_change_requests](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-change-requests) — Lists a project's change requests in the project's order with their reason, priority, impact level, status, baseline and the people behind them, narrowed by reason, status, priority, impact level, baseline or words; a guest token sees the requests they raised or asked for, without the effort, the cost and Heimdall's draft. - [list_comments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-comments) — The conversation on one artefact, oldest first, each comment with its author, its audience, the people it names, the thread it answers and whether the thread is resolved; the open comments only for a guest token. - [list_council_sessions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-council-sessions) — The sittings of Heimdall's council on a project, newest first, each with its moment, record, voices, status, verdict, counts, cost and turns; free to call. - [list_decisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-decisions) — A project's decisions in the project's order, with reference, kind, title, context, the options weighed, what was decided, consequences, status, the day decided, deciders and the decision each supersedes; never for a guest token. - [list_epics](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-epics) — A project's epics in the project's order, with reference, goal, owner, priority, status and dates, narrowed for a guest token. - [list_import_batches](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-import-batches) — Lists the brief imports of a project, newest first, with where each reading stands. - [list_links](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-links) — Every link that touches one artefact, both ends named with their keys and titles, each read from the artefact's side; never for a guest token. - [list_pending_approvals](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-pending-approvals) — Lists approval requests as the inbox shows them, waiting on the token holder by default, raised by them or everything they may see, with filters and paging. - [list_quality_reviews](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-quality-reviews) — The quality reviews Heimdall wrote of one work item, requirement or business rule, newest first, each with the version it read, its score, band, summary and findings; free to call. - [list_raid_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-raid-items) — A project's RAID log in the project's order, with reference, kind, title, area, probability, impact and score, mitigation and contingency, owner and raiser, dates, dependency and status; never for a guest token. - [list_requirements](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements) — A project's functional and non-functional requirements in the project's order, with reference, kind, category, statement, source, priority, measure, verification method, status and owner, narrowed for a guest token. - [list_research_notes](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-research-notes) — Reads what Heimdall's research sweep found about a project's domain, with the latest run's standing. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — Every version of a revisable artefact, newest first, with who wrote it, why, and the diff from the version before. - [list_use_cases](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-use-cases) — A project's use cases in the project's order, with reference, title, actor, other actors, goal, trigger, conditions, frequency, priority and status, narrowed for a guest token; the steps come with get_use_case. - [propose_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/propose-business-rule) — Add a business rule to a project as a draft a person reviews, with its kind, statement, condition and action, exceptions, worked examples, authority and priority. - [raise_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-change-request) — Raises a change request on a project, numbered from the change requests' own sequence, with its reason, the baseline it changes, the stakeholder who asked, the records it touches and whatever impact is known; a guest token raises one without the impact. - [raise_risk](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-risk) — Raise a risk in a project's RAID log, numbered from the log's sequence, scored by probability and impact, raised by the person the token acts as and open for the review meeting. - [record_decision_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/record-decision-draft) — Record a decision in a project as proposed, numbered from the decisions' sequence, with its context, the options weighed and what is proposed, for a person to accept or reject. - [remove_link](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/remove-link) — Removes a link between two artefacts after the person confirms; both artefacts stay. - [restore_revision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/restore-revision) — Put an earlier version of an artefact back, which writes a new version that names the one restored and announces it. - [review_quality](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/review-quality) — Asks Heimdall to review one work item, requirement or business rule and keeps the review, with its score, band, summary and findings; spends the team's AI budget. - [start_brief_import](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-brief-import) — Hands a whole brief to Heimdall's reader, so a document pasted into the conversation lands on the review queue as a scaffold of drafts. - [start_project_research](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-project-research) — Sends Heimdall to research a project's domain, so a conversation that opened a project can ask for the industry's expectations without leaving the panel. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — Puts an epic, story, requirement, business rule, use case, baseline or change request up for approval under the record's own submit ability, or resubmits it as the next round when its open request waits for changes. - [sweep_improvements](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/sweep-improvements) — Runs Heimdall's improvement sweep on a project and answers with how many gaps became cards, how many cards came down and how many wait; calls no model and spends nothing. - [update_approval_policy](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-approval-policy) — Writes a project's approval policy for one kind, after the person confirms; who decides, how many must agree and how long they have. - [update_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-baseline) — Changes a draft baseline's name, kind, words or records, or moves a frozen baseline to superseded; a frozen baseline's fields and records never change, and the approval engine's moves are refused. - [update_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-business-rule) — Changes a business rule in part and moves it where the lifecycle allows by hand; the review states stay with the approvers. - [update_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-change-request) — Changes a change request's words, records touched or impact while it is not yet decided, or moves it by hand along its lifecycle; the moves through the approval gate are refused. - [update_decision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-decision) — Changes a decision in part and moves it along its lifecycle when a status is sent; accepting stamps the date decided. - [update_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-epic) — Changes an epic in part and moves it where the lifecycle allows by hand; the review states stay with the approvers. - [update_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-raid-item) — Changes a RAID item in part and moves it along its lifecycle when a status is sent. - [update_requirement](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-requirement) — Changes a requirement in part and moves it where the lifecycle allows by hand; the kind is fixed and the review states stay with the approvers. - [update_use_case](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-use-case) — Changes a use case in part, replaces its steps when a list is sent and moves it where the lifecycle allows by hand. - [create_client](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-client) — Adds a client to the team's registry, the first record Heimdall writes when a person describes a new client's project in conversation. - [create_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-component) — Adds one of the applications a project is made of, so Heimdall can lay down the web app and the API of a new project on the house stack. - [create_design_reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-design-reference) — Adds a link to a design, so the Figma file or the API contract a person pastes into the conversation lands on the record. - [create_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-environment) — Adds one place a project runs, so Heimdall can lay down the development, staging, UAT and production rows of a new project. - [create_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-project) — Creates a project on the team's registry from a name and a paragraph, with the key suggested when none is given, so Heimdall can open a project in conversation. - [delete_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-component) — Deletes a component of a project after the person confirms; the environments and repositories under it lose their component. - [delete_design_reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-design-reference) — Deletes a design reference of a project after the person confirms; the design itself is untouched. - [delete_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-environment) — Deletes an environment of a project after the person confirms. - [get_client](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-client) — Fetch a single client by UUID — its name, contacts, status and notes. - [get_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-component) — Fetch a single component by UUID — its name, slug, kind, description, tech stack, owner and archive stamp. - [get_design_reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-design-reference) — One design reference by id, with its address, kind, what it is attached to, version, notes and who added it. - [get_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-environment) — Fetch a single environment by UUID — its kind, component, addresses, branch, flags, health, version and freeze, never a secret. - [get_project_settings](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project-settings) — A project's settings by project id, every key present with the workspace default filled in, from sprint length and working days to estimation, workflow flags, business hours and alert routing. - [get_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project) — Fetch a single project by UUID — its key, status, methodology, visibility, health, leads and dates. - [Project Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project) — A project is one client engagement's record: its key, which prefixes every reference inside it, its status from discovery to maintenance, its methodology, visibility, health and leads. - [list_clients](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-clients) — Paginated list of the clients the token's team does projects for, narrowed by status, search words or archived state. - [list_components](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) — Paginated list of the components a project is made of, with how many environments run each, narrowed by kind, search words or archived state. - [list_design_references](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-design-references) — Paginated list of the links a project keeps to its designs, each with its kind, what it is attached to, version and notes, narrowed by kind, component, whole-project scope or search words. - [list_environments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) — Paginated list of a project's environments in promotion order, with their addresses, branch, flags and health, narrowed by kind, component, health or search words. - [list_projects](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-projects) — Paginated list of the projects the token can see, narrowed by status, client, search words or archived state. - [update_client](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-client) — Changes a client on the team's registry in part, so a contact, a country or a status can be filled in as the conversation reveals it. - [update_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-component) — Changes a component of a project in part; a field left out keeps its value and the tags replace the list when sent. - [update_design_reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-design-reference) — Changes a design reference of a project in part. - [update_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-environment) — Changes an environment of a project in part; the health and the freeze have their own doors and never change here. - [update_project_settings](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project-settings) — Changes the settings every later module reads, from the sprint length to the council's seats, after the person confirms. - [update_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project) — Changes a project's record in part, so a conversation can add the launch date, the leads, the client or a status one answer at a time. - [Role & Group Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups) — A role is the permission set one member holds, seeded per team and editable by its owner; a group is a bundle of members with permissions they share. - [list_groups](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-groups) — List groups across every team the caller owns or belongs to, with the permissions each group shares and the members in it. - [list_roles](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-roles) — List roles across every team the caller owns or belongs to. Each role carries a flat permission list and whether it was seeded. - [Team Member Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members) — Team members are the staff and guests who collaborate inside a team, each with a role that decides what they may do. - [list_team_members](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members/list-team-members) — List the people on a team, the owner first, each with the role they hold there. Email addresses only for a token that may view a member. - [get_team](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/get-team) — Fetch a single team by UUID from the caller's visibility set (owned or joined). A foreign team answers RESOURCE_NOT_FOUND. - [Team Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team) — A team groups the people, roles and projects of one organisation, and every token is scoped to exactly one. - [list_teams](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/list-teams) — List every team the authenticated person owns or belongs to. Read-only — changing a team stays in the application and the REST API. - [Token Tools](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens) — Personal access tokens authorise every REST and MCP call, and the plaintext value exists exactly once. - [list_tokens](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/list-tokens) — List the authenticated person's own personal access tokens with abilities and scope tuples split out. The plaintext value is never shown. - [revoke_token](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/revoke-token) — Revoke one of the authenticated person's own tokens by id. Self-revocation is refused. - [MCP Troubleshooting](https://alm-docs.labs.zuselab.dev/mcp/v1/troubleshooting) — Common MCP failure modes, from a client that cannot connect to a tool that refuses an ability, and how to resolve each. ## planning - [Acceptance Criteria](https://alm-docs.labs.zuselab.dev/planning/acceptance-criteria) — The acceptance criteria rows on a work item: one sentence each with an optional Given, When and Then and a kind, written in the item's editor, ordered, versioned with the item and replaced over the API. - [Approvals](https://alm-docs.labs.zuselab.dev/planning/approvals) — Submit an epic, story, requirement, rule or use case for review, who decides under the project's policy, the inbox, resubmitting after changes, withdrawing, the clocks, and what a client guest signs. - [Attachments](https://alm-docs.labs.zuselab.dev/planning/attachments) — The Files panel in every artefact flyout: pin wireframes, signed specifications and sample files on epics, stories, requirements, rules, use cases, RAID items, decisions and dictionary entries, read them through the application, and remove them. - [Backlog](https://alm-docs.labs.zuselab.dev/planning/backlog) — The Backlog page of a project: every story, task, sub-task, bug, spike and improvement with its reference, kind, status, priority, people, estimates and the fields its kind adds, its lifecycle, its hand-over and its versions. - [Baselines](https://alm-docs.labs.zuselab.dev/planning/baselines) — Compose a baseline from approved records, submit it for the project manager's approval that freezes it, have the client sign a scope baseline, read what it holds, supersede it, and understand the lock a frozen baseline puts on every record it holds. - [Brief import](https://alm-docs.labs.zuselab.dev/planning/brief-import) — Paste a brief, let Heimdall draft epics, stories, requirements, rules, terms, personas, risks and decisions from it, and accept, edit, merge or reject each suggestion from a review queue; nothing lands until you say so. - [Business Rules](https://alm-docs.labs.zuselab.dev/planning/business-rules) — The Business Rules page of a project: the decisions of the business the system must honour, each with its kind, statement, condition and action, exceptions, worked examples, authority, priority, dates in force, status and owner, its review lifecycle and its versions. - [Change Requests](https://alm-docs.labs.zuselab.dev/planning/change-requests) — Raise a change request against a frozen baseline, name the records it touches, write or ask Heimdall for the impact on scope, schedule, effort, cost and risk, convene the council, submit it to the approvers whose agreement unlocks the records named, and see the change through to verified. - [Comments and mentions](https://alm-docs.labs.zuselab.dev/planning/comments-and-mentions) — The Discussion panel in every artefact flyout: ask and answer in threads, name people with @ to tell them, keep a comment internal, resolve a thread, and follow an artefact to hear of every comment on it. - [Decisions](https://alm-docs.labs.zuselab.dev/planning/decisions) — The Decisions page of a project: every choice the project made with its kind, context, the options weighed, what was decided, the consequences, who decided and when, the decision it supersedes, the lifecycle from proposed to accepted and the versions. - [Epics](https://alm-docs.labs.zuselab.dev/planning/epics) — The Epics page of a project: each body of work with its reference, goal, value, measures, owner, MoSCoW priority, status, dates and colour, its lifecycle and its versions. - [Functional Requirements](https://alm-docs.labs.zuselab.dev/planning/functional-requirements) — The Requirements page of a project: what the system shall do, each requirement with its reference, statement, rationale, source, priority, verification method, status and owner, its review lifecycle and its versions. - [Planning](https://alm-docs.labs.zuselab.dev/planning) — The pages where a project's scope is written down: epics, the backlog of stories, tasks, bugs and spikes with their acceptance criteria, the functional and non-functional requirements, the business rules, the use cases with their flows, the RAID log, the decisions, the links between them, the comments and the files on each, every save kept as a restorable version, and the approvals that turn a draft into an agreed version; the rest as it lands. - [Labels](https://alm-docs.labs.zuselab.dev/planning/labels) — A project's own words pinned on its work items: pick labels on an item, filter the backlog by one, and shape the vocabulary from the backlog's label manager with a colour and a meaning for each word. - [Non-Functional Requirements](https://alm-docs.labs.zuselab.dev/planning/non-functional-requirements) — The qualities a system shall keep: each non-functional requirement with the category it belongs to and the measure that says when it is met, the categories, how to write a target and what reads them. - [Quality review](https://alm-docs.labs.zuselab.dev/planning/quality-review) — Heimdall reads a story, a requirement or a business rule against INVEST, clear wording, testability, the project's business rules and its siblings, scores it out of 100 and lists what to fix; nothing changes until you do. - [RAID Log](https://alm-docs.labs.zuselab.dev/planning/raid-log) — The RAID Log page of a project: risks scored by probability and impact with their mitigation and contingency, assumptions, issues and dependencies, each with its area, owner, dates and status, the risk matrix, the lifecycle and the versions. - [Revisions and Versions](https://alm-docs.labs.zuselab.dev/planning/revisions-and-versions) — Every save of a planning artefact, by a person or by Heimdall, writes a version with its diff, author and reason; read them in the flyout, the API or the tools, and restore any. - [Traceability](https://alm-docs.labs.zuselab.dev/planning/traceability) — The Links panel in every artefact flyout: join a story to the requirement it implements, a bug to the story that fixes it, a decision to the epic it shapes, read each link from either side and open the other end. - [Use Cases](https://alm-docs.labs.zuselab.dev/planning/use-cases) — The Use Cases page of a project: how each actor reaches a goal with the system, with the actor, the goal, the trigger, the conditions before and after, the steps of the main flow and the alternate and exception flows that leave it, the review lifecycle and the versions. ## projects - [Clients](https://alm-docs.labs.zuselab.dev/projects/clients) — The Clients page: the organisations your team does projects for, adding one with its contacts and status, editing it, archiving and restoring it, and why a client with projects is never deleted. - [Components](https://alm-docs.labs.zuselab.dev/projects/components) — The Components page of a project: the web app, the API, the mobile apps and the services it is made of, each with a slug, a kind, a tech stack and an owner; adding, editing, archiving, restoring and deleting them. - [Data Dictionary](https://alm-docs.labs.zuselab.dev/projects/data-dictionary) — The Data Entities tab of a project's Dictionary pages: the things the project keeps records of, their fields, owners, personal data, retention and review status. - [Design References](https://alm-docs.labs.zuselab.dev/projects/design-references) — The Designs page of a project: the links it keeps to Figma files, wireframes, prototypes, screenshots, API contracts and diagrams, attached to the whole project or to one component. - [Environments](https://alm-docs.labs.zuselab.dev/projects/environments) — The Environments page of a project: development, staging, UAT and production with their addresses, branch and flags; the scheduled health check; freezing changes; and what a client guest sees of it. - [Glossary](https://alm-docs.labs.zuselab.dev/projects/glossary) — The Glossary tab of a project's Dictionary pages: the words the project agrees on, with definitions, other names, related terms and a review status. - [Projects](https://alm-docs.labs.zuselab.dev/projects) — What a project is in Zuse Bifrost, the key that prefixes every reference in it, its lifecycle statuses and health, the client it is done for, and the pages that manage them. - [The Intake Wizard](https://alm-docs.labs.zuselab.dev/projects/intake-wizard) — Start a project from a client, a name and a paragraph, then add components, environments, stakeholders and personas step by step, resuming any time. - [The Project Overview](https://alm-docs.labs.zuselab.dev/projects/overview) — Opening a project from the selector, the registry or a Heimdall button; what the overview shows: the key, status and health, the lifecycle strip, the Heimdall suggests rail, the client, dates, leads, budget, description and recent activity; and what a client guest sees of it. - [Personas](https://alm-docs.labs.zuselab.dev/projects/personas) — The Personas tab of a project's People pages: the archetypal users the project is built for, with their goals, frustrations, habits, proficiency and devices; adding, editing, archiving, restoring and deleting them. - [Project Settings](https://alm-docs.labs.zuselab.dev/projects/project-settings) — The Settings page of a project: cadence, estimation, workflow, collaboration and alert knobs with their defaults, plus the danger zone. - [The Registry](https://alm-docs.labs.zuselab.dev/projects/registry) — The Projects page: every project you may see with its filters, starting one in the intake wizard, editing it, moving its status, archiving, restoring and deleting it, and reading its history. - [Repositories](https://alm-docs.labs.zuselab.dev/projects/repositories) — The Repositories tab of a project's Code pages: the Git repositories the code lives in, with provider, owner, name, default branch, privacy and component; registering, editing, disconnecting and deleting them. - [Stakeholders](https://alm-docs.labs.zuselab.dev/projects/stakeholders) — The Stakeholders tab of a project's People pages: the client's sponsor, product owner and approvers, Zuse's own leads and third parties, each with a side, a role, a weight and the approvals that name them; adding, editing and removing them, and what a guest sees. ## reference - [Changelog](https://alm-docs.labs.zuselab.dev/reference/changelog) — Every change that reached Zuse Bifrost, newest first, dated by the day it landed and linked to the pages of this guide that describe it. - [Status Reference](https://alm-docs.labs.zuselab.dev/reference/status-reference) — Every status badge the application shows, enum by enum: client and project lifecycle statuses, project health, the review of a glossary term or data entity, design reference kinds, Git providers, epic statuses and priorities, work item kinds, statuses, resolutions, priorities, bug severities and criterion kinds, requirement kinds, categories, verification methods and statuses, business rule kinds, use case flows, RAID kinds, areas and statuses, decision kinds and statuses, baseline kinds and statuses, change reasons, change request statuses and impact levels, document statuses, sprint statuses, link kinds, attachment kinds, quality bands and finding severities, import, research, council sitting, council finding and suggestion statuses, suggestion kinds, revision reasons, webhook delivery states and the states of a Heimdall action. ## webhooks - [ai.budget_exhausted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai/ai-budget-exhausted) — The month's spend reaches the cap. - [ai.budget_warning](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai/ai-budget-warning) — The month's spend crosses the budget's alert threshold. - [ai.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai) — The team's monthly AI budget approaching and reaching its cap. - [approval.approved](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-approved) — A request carries. - [approval.changes_requested](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-changes-requested) — An approver asks for changes. - [approval.decided](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-decided) — One approver gives a verdict. - [approval.escalated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-escalated) — A pending request passes the time the policy allows. - [approval.policy_updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-policy-updated) — A project changes how one kind of approval is decided. - [approval.rejected](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-rejected) — An approver rejects the request. - [approval.requested](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-requested) — An artefact is put up for approval. - [approval.withdrawn](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-withdrawn) — An open request is taken back. - [approval.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval) — The life of an approval request: an artefact put up for approval, each approver's verdict, the outcome the verdicts settle, a request taken back, a request that ran past its time, and a change to how a project decides a kind of approval. - [attachment.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment/attachment-created) — A file is uploaded onto an artefact. - [attachment.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment/attachment-deleted) — A file is removed. - [attachment.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment) — The files pinned on a project's artefacts: a file uploaded and a file removed. - [baseline.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-created) — A baseline is added. - [baseline.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-deleted) — A draft baseline is deleted. - [baseline.frozen](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-frozen) — The team's approval froze a baseline. - [baseline.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-restored) — An earlier version of a draft baseline is put back. - [baseline.signed_off](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-signed-off) — The client signed a scope baseline. - [baseline.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-status-changed) — A baseline moves between its states. - [baseline.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-updated) — A draft baseline's words or records change. - [baseline.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline) — The life of a project's baselines: a baseline added as a draft, its words or items changed, moved by hand, frozen by the team's approval, signed by the client, put back to an earlier version and deleted. - [business_rule.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-created) — A rule is added. - [business_rule.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-deleted) — A rule is deleted. - [business_rule.quality_reviewed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-quality-reviewed) — A rule's quality is reviewed. - [business_rule.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-restored) — An earlier version of a rule is put back. - [business_rule.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-status-changed) — A rule moves along its review lifecycle. - [business_rule.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-updated) — A rule's fields change. - [business_rule.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule) — The lifecycle of a project's business rules: a rule added, changed, moved along its review lifecycle, put back to an earlier version and deleted. - [change_request.analysed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-analysed) — Heimdall read a change request against its baseline. - [change_request.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-created) — A change request is raised. - [change_request.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-deleted) — A change request that was never agreed is deleted. - [change_request.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-restored) — An earlier version of a change request is put back. - [change_request.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-status-changed) — A change request moves between its states. - [change_request.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-updated) — A change request's fields change. - [change_request.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request) — The life of a project's change requests: a request raised, its words or impact changed, moved along its lifecycle by hand or by the approval engine, read by Heimdall for its impact, put back to an earlier version and deleted. - [client.archived](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-archived) — A client is archived. - [client.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-created) — A client is added. - [client.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-deleted) — A client that no project names is deleted. - [client.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-restored) — An archived client is restored. - [client.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-updated) — A client's fields change. - [client.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client) — The lifecycle of a client: creation, edits, archiving, restoring and deletion. - [comment.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-created) — A comment is posted. - [comment.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-deleted) — A comment is deleted. - [comment.reopened](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-reopened) — A resolved thread is marked open again. - [comment.resolved](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-resolved) — The first comment of a thread is marked resolved. - [comment.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-updated) — A comment's words are rewritten. - [comment.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment) — The conversations on a project's artefacts: a comment posted, edited, resolved, reopened and deleted. - [component.archived](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-archived) — A component is archived. - [component.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-created) — A component is added. - [component.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-deleted) — A component no environment runs is deleted. - [component.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-restored) — An archived component is restored. - [component.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-updated) — A component's fields change. - [component.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component) — The lifecycle of a project's components: creation, edits, archiving, restoring and deletion. - [council.convened](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-convened) — council.convened - [council.failed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-failed) — council.failed - [council.finding_accepted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-accepted) — council.finding_accepted - [council.finding_dismissed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-dismissed) — council.finding_dismissed - [council.ready](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-ready) — council.ready - [council.round_opened](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-round-opened) — council.round_opened - [council.voice_spoke](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-voice-spoke) — council.voice_spoke - [council.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council) - [data_entity.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-created) — An entity is created. - [data_entity.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-deleted) — An entity is deleted. - [data_entity.fields_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-fields-changed) — The entity's fields are replaced. - [data_entity.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-restored) — An earlier version of a entity is put back. - [data_entity.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-status-changed) — An entity moves along its review. - [data_entity.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-updated) — An entity's fields change. - [data_entity.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity) — The lifecycle of a project's data dictionary: an entity created, changed, moved along its review, its fields replaced and the entity deleted. - [decision.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-created) — A decision is added. - [decision.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-deleted) — A decision is deleted. - [decision.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-restored) — An earlier version of a decision is put back. - [decision.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-status-changed) — A decision moves along its lifecycle. - [decision.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-updated) — A decision's fields change. - [decision.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision) — The lifecycle of a project's decisions: a decision added, changed, moved along its lifecycle, put back to an earlier version and deleted. - [design_reference.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-created) — A reference is added. - [design_reference.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-deleted) — A reference is deleted. - [design_reference.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-updated) — A reference's fields change. - [design_reference.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference) — The lifecycle of a project's design references: a link added, changed and removed. - [environment.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-created) — An environment is added. - [environment.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-deleted) — An environment is deleted. - [environment.frozen](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-frozen) — A change freeze starts. - [environment.health_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-health-changed) — A check finds the environment in a different health than before. - [environment.unfrozen](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-unfrozen) — A change freeze is lifted before it ends. - [environment.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-updated) — An environment's fields change. - [environment.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment) — The lifecycle and the state of a project's environments: creation, edits, deletion, change freezes and the health the checks find. - [epic.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-created) — An epic is added. - [epic.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-deleted) — An epic is deleted. - [epic.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-restored) — An earlier version of an epic is put back. - [epic.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-status-changed) — An epic moves along its lifecycle. - [epic.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-updated) — An epic's fields change. - [epic.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic) — The lifecycle of a project's epics: an epic added, changed, moved along its lifecycle, put back to an earlier version and deleted. - [glossary_term.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-created) — A term is added. - [glossary_term.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-deleted) — A term is deleted. - [glossary_term.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-restored) — An earlier version of a term is put back. - [glossary_term.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-status-changed) — A term moves along its review. - [glossary_term.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-updated) — A term's fields change. - [glossary_term.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term) — The lifecycle of a project's glossary: a term added, changed, moved along its review and deleted. - [import.discarded](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-discarded) — A person dropped the batch. - [import.failed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-failed) — The reading failed. - [import.ready](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-ready) — Heimdall read the brief. - [import.started](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-started) — A brief is handed to Heimdall. - [import.suggestion_accepted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-accepted) — A person accepted a suggestion. - [import.suggestion_merged](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-merged) — A person merged a suggestion into a record the project already held. - [import.suggestion_rejected](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-rejected) — A person rejected a suggestion, or dismissed a note. - [import.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import) — The life of a brief import: a brief handed to Heimdall, read into suggestions or failed, discarded, and each suggestion accepted, rejected or merged by a person. - [improvement.swept](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/improvement/improvement-swept) — The sweep ran on a project. - [improvement.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/improvement) — Heimdall's improvement sweep of a project's record: one event each time it ran, with what it proposed, took down and left waiting. - [Event Reference](https://alm-docs.labs.zuselab.dev/webhooks/v1/events) — Every outbound webhook event, generated from the OpenAPI document and grouped by family. - [label.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label) — A project's label vocabulary: a label added, rewritten and removed. - [label.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-created) — A word is added to the vocabulary. - [label.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-deleted) — A label is removed. - [label.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-updated) — A label's word, colour or note changes. - [link.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link) — The links between a project's artefacts: a link made and a link removed. - [link.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-created) — Two artefacts are linked. - [link.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-deleted) — A link is removed. - [persona.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona) — The lifecycle of a project's personas: creation, edits, archiving, restoring and deletion. - [persona.archived](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-archived) — A persona is archived. - [persona.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-created) — A persona is added. - [persona.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-deleted) — A persona is deleted. - [persona.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-restored) — An archived persona is restored. - [persona.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-updated) — A persona's fields change. - [persona.version_restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-version-restored) — An earlier version of a persona is put back. - [project.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project) — The lifecycle of a project and its membership: creation, edits, status moves, archiving, restoring, deletion, and people joining or leaving it. - [project.archived](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-archived) — A project is archived. - [project.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-created) — A project is created. - [project.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-deleted) — A project is deleted with everything under it. - [project.member_added](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-member-added) — A person gains a membership row on the project. - [project.member_removed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-member-removed) — A person's membership row on the project is removed. - [project.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-restored) — An archived project is restored. - [project.settings_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-settings-changed) — One or more of the project's settings change. - [project.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-status-changed) — A project moves between lifecycle statuses. - [project.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-updated) — A project's own fields change. - [raid_item.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item) — The lifecycle of a project's RAID log: a risk, an assumption, an issue or a dependency raised, changed, moved along its lifecycle, put back to an earlier version and deleted. - [raid_item.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-created) — An item is raised. - [raid_item.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-deleted) — An item is deleted. - [raid_item.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-restored) — An earlier version of an item is put back. - [raid_item.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-status-changed) — An item moves along its lifecycle. - [raid_item.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-updated) — An item's fields change. - [repository.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository) — The lifecycle of a project's repositories: registration, edits, disconnecting, reconnecting and deletion. - [repository.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-created) — A repository is registered. - [repository.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-deleted) — A repository is deleted. - [repository.disconnected](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-disconnected) — A repository is set aside. - [repository.reconnected](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-reconnected) — A disconnected repository is brought back. - [repository.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-updated) — A repository's fields change. - [requirement.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement) — The lifecycle of a project's requirements: a functional or non-functional requirement added, changed, moved along its review lifecycle, put back to an earlier version and deleted. - [requirement.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-created) — A requirement is added. - [requirement.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-deleted) — A requirement is deleted. - [requirement.quality_reviewed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-quality-reviewed) — A requirement's quality is reviewed. - [requirement.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-restored) — An earlier version of a requirement is put back. - [requirement.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-status-changed) — A requirement moves along its review lifecycle. - [requirement.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-updated) — A requirement's fields change. - [research.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research) — The life of a research run: Heimdall asked to read a project's domain, done with notes and suggestions, or failed. - [research.failed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-failed) — The run failed. - [research.ready](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-ready) — Heimdall finished the research. - [research.started](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-started) — A person asks Heimdall to research the project. - [role.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role) — Role creation, edits and deletion. - [role.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-created) — A role is created. - [role.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-deleted) — A custom role is deleted. - [role.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-updated) — A role's name, description or permissions change. - [stakeholder.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder) — The lifecycle of a project's stakeholders: who was added to the record, what changed about them and who was removed. - [stakeholder.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-created) — A stakeholder is added. - [stakeholder.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-deleted) — A stakeholder is removed. - [stakeholder.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-restored) — An earlier version of a stakeholder is put back. - [stakeholder.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-updated) — A stakeholder's fields change. - [team.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team) — Team creation, rename and deletion, and the comings and goings of the people in it. - [team.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-created) — A team is created. - [team.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-deleted) — A team with no projects is deleted. - [team.member_invited](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-invited) — An invitation is sent to a staff member or a client guest. - [team.member_joined](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-joined) — An invitee accepts and becomes a member. - [team.member_removed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-removed) — A member is removed from the team. - [team.member_role_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-role-changed) — A member moves to another of the team's roles. - [team.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-updated) — A team is renamed. - [token.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token) — Personal access token minting and revocation. - [token.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token/token-created) — A token is minted. - [token.revoked](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token/token-revoked) — A token is revoked. - [use_case.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case) — The lifecycle of a project's use cases: a use case added, changed, moved along its review lifecycle, given new steps, put back to an earlier version and deleted. - [use_case.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-created) — A use case is added. - [use_case.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-deleted) — A use case is deleted. - [use_case.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-restored) — An earlier version of a use case is put back. - [use_case.status_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-status-changed) — A use case moves along its review lifecycle. - [use_case.steps_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-steps-changed) — A use case's steps are replaced. - [use_case.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-updated) — A use case's fields change. - [work_item.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item) — The lifecycle of a project's work items: a story, task, sub-task, bug, spike or improvement added, changed, moved along its lifecycle, handed to a person, given new acceptance criteria, put back to an earlier version and deleted. - [work_item.assigned](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-assigned) — An item is handed to a person, or to nobody. - [work_item.created](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-created) — An item is added. - [work_item.criteria_changed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-criteria-changed) — An item's acceptance criteria are replaced. - [work_item.deleted](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-deleted) — An item is deleted. - [work_item.quality_reviewed](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-quality-reviewed) — An item's quality is reviewed. - [work_item.restored](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-restored) — An earlier version of an item is put back. - [work_item.transitioned](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-transitioned) — An item moves along its lifecycle. - [work_item.updated](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-updated) — An item's fields change. - [Webhooks & Events](https://alm-docs.labs.zuselab.dev/webhooks/v1) — Event-driven HTTPS notifications from Zuse Bifrost to your server, CI or anything that takes JSON over POST, with retries and replay. - [Webhooks Quickstart](https://alm-docs.labs.zuselab.dev/webhooks/v1/quickstart) — Register a target, subscribe to an event, receive a signed payload, verify it and respond 200, end to end. - [Retries & Delivery](https://alm-docs.labs.zuselab.dev/webhooks/v1/retries-and-delivery) — The exponential back-off schedule, dead-lettering, the auto-disable threshold and how to replay a failed delivery. - [Webhook Security](https://alm-docs.labs.zuselab.dev/webhooks/v1/security) — Secret rotation, replay windows, the IP allow-list and what to trust on an inbound delivery, so nothing can spoof or replay a Zuse Bifrost event. - [Signature Verification](https://alm-docs.labs.zuselab.dev/webhooks/v1/signature-verification) — HMAC-SHA256 verification of every delivery's ZG-Signature header, in a Stripe-compatible scheme, and how a rotation replaces the secret. - [Testing & Debugging Webhooks](https://alm-docs.labs.zuselab.dev/webhooks/v1/testing-and-debugging) — Fire a test payload, read the delivery log and retry a failed delivery by hand, without waiting for the production trigger. ## workspace - [Ability Reference](https://alm-docs.labs.zuselab.dev/workspace/abilities) — The complete list of permissions a role or a group can grant, what each entity covers, and a plain sentence on what a member holding each permission can do. - [Activity Log](https://alm-docs.labs.zuselab.dev/workspace/activity-log) — Read every change recorded in your team, see who made it and whether it came from a person, a token, an agent, an integration or the system, and open an item's full history with its field-by-field changes. - [AI Settings](https://alm-docs.labs.zuselab.dev/workspace/ai-settings) — Read where the team's AI budget stands this month, set the cap and the warning line, read the statement per feature and model and the latest calls, and see which model each job runs on. - [API Tokens](https://alm-docs.labs.zuselab.dev/workspace/api-tokens) — Mint a personal access token for the REST API, the MCP server or a pipeline, scope it to a team and optionally one project, tick its abilities, and revoke it. - [Groups](https://alm-docs.labs.zuselab.dev/workspace/groups) — Grant a set of permissions to several members at once, on top of their roles, and keep the group's membership in step with the team. - [Your Workspace](https://alm-docs.labs.zuselab.dev/workspace) — How a workspace is organised: teams that own projects, the people in them, the roles that say what each person may do, and groups that grant more. - [Members and Guests](https://alm-docs.labs.zuselab.dev/workspace/members-and-guests) — Invite a colleague with a role or a client as a guest of one project, change a member's role, remove someone, and manage the invitations still waiting. - [Roles](https://alm-docs.labs.zuselab.dev/workspace/roles) — The eight default roles every team starts with, how to create and edit custom roles, the permission catalogue, and the rules on deleting a role. - [Teams](https://alm-docs.labs.zuselab.dev/workspace/teams) — Create a team, make one of yours current, open its members, and rename or delete the teams you own, with what each action changes. - [Webhooks](https://alm-docs.labs.zuselab.dev/workspace/webhooks) — Register an endpoint that receives signed event payloads, pick its events, rotate its secret, pause or test it, and read and replay the delivery log. --- # Appearance Source: https://alm-docs.labs.zuselab.dev/account/appearance By the end of this page you will know how to set the colour scheme of the application and where the choice is kept. Appearance settings live under **Settings → Appearance**. The same Light and Dark switch sits at the bottom of the sidebar on every page, so you rarely need the settings page for it. ## The three options | Option | What it does | | ---------- | -------------------------------------------------------------------------------------- | | **Light** | Always the light theme. | | **Dark** | Always the dark theme. The application starts in dark mode until you choose otherwise. | | **System** | Follows whatever the device is set to, and switches when the device does. | ## Change the scheme [step] 1. Open **Settings → Appearance**. 2. Pick **Light**, **Dark** or **System**. 3. The change applies immediately. There is nothing to save. The choice is kept in the browser you made it in, not on your account. A second device, or a different browser on the same device, keeps its own setting; pick again there. ## Related - [Timezone and language](/account/timezone-and-locale): the other display settings, which do follow your account. - [Profile](/account/profile): your name and job title. --- # Your Account Source: https://alm-docs.labs.zuselab.dev/account Your account is the person Zuse Bifrost records against everything you do: the stories you write, the approvals you decide, the time you log, the releases you promote. This section covers the account itself, from the invitation that creates it to the ways you sign in, the protections around them, and the personal settings that follow you from device to device. ## How an account comes to exist There is no sign-up page. Someone in a team invites you by email, and accepting the invitation creates your account with the name and job title the invitation carried. A client stakeholder is invited the same way, as a guest of one project, and sees only what that project shares with them. [Accepting an invitation](/account/invitations) walks through what the link does for a new address, for an existing account, and when you are signed in as someone else. If you were expecting an invitation and nothing arrived, check your spam folder first, then ask the person who invited you to send it again. An invitation is tied to the address it was sent to. ## Signing in and staying safe ## Preferences ## Where the settings live Open the account menu at the bottom of the sidebar (top right on a phone) and choose **Account Settings**. The settings pages share one navigation: **Profile**, **Security**, **Teams**, **Roles**, **Groups**, **Appearance**, **Localization**, **API Tokens** and **Webhooks**. Profile, Security, Appearance and Localization are about you and are covered in this section; the rest are about your [workspace](/workspace), including the [tokens](/workspace/api-tokens) and [webhooks](/workspace/webhooks) that connect pipelines and agents to it. Notifications join as the workspace grows. The same account menu holds the team switcher, which lists every team you belong to with the current one marked, and a **Manage Teams** link to the [teams page](/workspace/teams). ## Security at a glance Every person is asked to hold at least two of these: a strong password, a second factor from an authenticator app, and a passkey. A passkey on its own is enough for daily sign-in; the password and the code are the ways back in when a device is lost. ## Accounts are personal One person, one account. Colleagues who need access are invited to the team with a role of their own, and clients are invited as guests of a project; nobody needs to share a sign-in. Every action in the activity log names the account that took it, which is only useful while that account is one person. --- # Accepting an Invitation Source: https://alm-docs.labs.zuselab.dev/account/invitations By the end of this page you will know what the invitation email contains, what the link does for you depending on whether you already have an account, and what to do when the page says the invitation is not yours or no longer valid. An invitation is the only way into Zuse Bifrost. Someone in a team invites your email address, with the role you will hold or, for a client, the project you are invited to, and you receive an email titled _You are invited to <team> on ZuseGenius_ with one button: **Accept the invitation**. ## The link is signed The address behind the button carries a signature. It works only as it was sent, and only while the invitation is pending: once accepted or withdrawn, it leads to a page that says so. Because the link reached your inbox, following it proves you own that address, which is why an account created from it starts verified and you are never sent a second email to confirm. Do not forward the email. Whoever follows the link can accept the invitation in your name. If you forwarded it by mistake, ask the person who invited you to withdraw it and send a new one. ## If you are new [step] 1. Follow **Accept the invitation**. The page is titled _Join <team>_ and shows the role you were invited as, the project when you are a guest, and who invited you. 2. Check your **Email**. It is the address the invitation was sent to and cannot be changed here; a different address needs a different invitation. 3. Confirm **Your name**. It is pre-filled when the inviter gave one, and you can correct it. 4. Choose a **Password** and type it again under **Confirm password**. 5. Press **Create Account and Join**. Your account is created, already verified, with the job title the inviter typed if they gave one; you join the team under the offered role; and you are signed in and taken to the dashboard, working within that team. A guest also receives the membership of the project they were invited to, and sees that project and nothing else. Afterwards, [Account Settings](/account) is where you add a [passkey](/account/passkeys) or turn on [two-factor authentication](/account/two-factor-auth). ## If you already have an account [step] 1. Follow **Accept the invitation**. 2. **Signed out:** you are sent to the sign-in page with a note that signing in accepts your invitation. Sign in with your password or a passkey and answer the two-factor challenge if you have one. The invitation is accepted the moment the sign-in completes, and you land on the dashboard. 3. **Signed in as the invited address:** you join the team the moment the page opens and are taken to the dashboard. If you had no current team before, the team you joined becomes current. Otherwise your current team stays as it was, and the new team appears in the switcher in the account menu. ## If the page says the invitation is for someone else You followed the link while signed in to an account whose address is not the invited one. The page names both addresses and offers **Sign out**, after which you can open the link again and sign in or sign up as the invited address, and **Back to the dashboard** if you only wanted to check. Nothing was accepted. If the invitation should have been sent to the address you are signed in with, ask the inviter to withdraw it and invite that address instead. ## If the page says the invitation is no longer valid The invitation was withdrawn, was already accepted, or the link lost part of its address on the way. Ask whoever invited you to send a new one; a new invitation is a new link. ## All the fields Shown, not editable. The address the invitation was sent to, already in lower case. It becomes your sign-in address. Required, between 2 and 255 characters. Pre-filled when the inviter gave a name. Shown on everything you do and editable later under Profile. Required and identical. The password policy of the workspace applies: in production, at least twelve characters with mixed case, letters, numbers and symbols, and not one found in a known breach. ## What happens next - You hold the invited role in the team; a guest additionally holds a membership of the invited project, marked as external. - The invitation disappears from the team's pending list. - The team's webhook subscribers receive `team.member_joined`, and for a guest `project.member_added` as well. - Your account's timezone starts as the workspace default; the [timezone detector](/account/timezone-and-locale) offers your real one on your first page load. ## Permissions | Action | Who may do it | | ---------------------- | --------------------------------------------------------- | | Accept an invitation | The owner of the invited address, signed in or signing up | | Withdraw or resend one | A member of the inviting team with the invite permission | ## Frequently asked No. The invitation and its link belong to the address it was sent to. Ask the inviter for an invitation to the address you want to use. A new team may have no projects yet, and a guest sees only the project named on the invitation. Ask the person who invited you which project to expect. Both. The first you accepted became your current team if you had none; the switcher in the account menu moves you between them. It stays valid until the invitation is accepted or withdrawn. The inviter sees it under Pending Invitations until then. ## Related - [Signing in](/account/sign-in): the sign-in that completes an invitation for an existing account. - [Members and guests](/workspace/members-and-guests): the inviter's side. - [Your account](/account): what an account is and how to protect it. --- # Notifications Source: https://alm-docs.labs.zuselab.dev/account/notifications By the end of this page you will know where every alert Zuse Bifrost sends you is kept, how to work through the Notifications Center, and how to decide which kinds of alert reach your email and, once linked, your chat. An alert is one thing the workspace tells you: a story waiting for your approval, a token minted on your account, an incident opened on a project you follow. Every alert belongs to a **class**, lands in your Notifications Center, and is delivered to the destinations you chose for that class. ## The Notifications Center The **Notifications Center** entry sits at the top of the sidebar, under the logo. It carries the number of alerts you have not opened and pops out the newest eight; press one to go where it points, or the tick to **Mark All as Read**. The number moves the moment an alert arrives, without reloading. **Open Notifications Center** is the full page: how many are unread, how many there are, when the last one arrived, and every alert grouped by day in your timezone, newest first. | Control | What it does | | ------------------------- | ------------------------------------------------------- | | **All / Unread / Read** | Narrow the list to what you have or have not opened. | | **Class** | Show one class of alert, such as Approvals or Security. | | **Search** | Find words in an alert's title or body. | | **Open** | Mark the alert read and go where it points. | | **Mark as Read** | Mark it read and stay. | | **Mark All as Read** | Clear the unread count in one go. | | **Notification Settings** | Jump to the destinations grid described below. | An alert that points nowhere, such as a notice about something already gone, shows no **Open** button. ## Choose where alerts reach you [step] 1. Open **Account Settings → Notifications**. The **Telegram** card at the top links your account to the bot; [Telegram](/integrations/telegram) describes it. 2. The **Alert Destinations** grid lists one row per class and one switch per destination: your **Email**, and your **Telegram** chat once it is linked. 3. Switch a cell on to have that class delivered there, off to stop. The email's **Incidents**, **Security** and **AI budget** cells are locked on. 4. Press **Save Changes**. Until you change the grid you are on the default: every class reaches your linked chat when you have one, and email carries whatever nothing else does. Switching every cell of a destination off keeps it in the grid and sends it nothing, so the next alert never falls back on it by surprise. Email is the safety net. A class that no other destination carries is mailed, and the three critical classes are mailed whatever you pick, because they arrive precisely when the chat you would otherwise read them on may have stopped working. ## The classes | Class | What it covers | | --------------- | --------------------------------------------------------------------------------------------- | | **Approvals** | Something waits for your decision, and the decisions made on what you submitted. | | **Assignments** | Work assigned to you, reassigned, or due soon. | | **Mentions** | Someone named you in a comment or an update. | | **Planning** | Epics, stories, requirements and rules created or changed on your projects. | | **Sprints** | Sprints planned, started and closed, and scope that changed mid-sprint. | | **Code** | Branches, commits and pull requests linked to your items. | | **Testing** | Test runs started and completed, failed executions and new defects. | | **Releases** | Releases planned, frozen, approved, published and rolled back. | | **Deployments** | Deployments queued, finished and failed on the environments you follow. | | **Incidents** | Incidents opened, updated, breached and resolved, with the public status line. Always mailed. | | **Support** | Support tickets raised, answered and waiting on you. | | **Security** | Sign-ins, passkeys, two-factor changes and tokens minted. Always mailed. | | **Digest** | Your Monday project digest, and an alert when a project breaks pattern. | | **Onboarding** | Nudges to finish setting up your projects. | | **AI budget** | The team's AI budget nearing or reaching its monthly cap. Always mailed. | The classes exist from the first day so your choices are kept as each phase of the product lands; today the Security class is the one that fires, when a [personal access token](/workspace/api-tokens) is minted on your account. ## All the fields Your account's address, from your profile. One switch per class, three of them locked on. Stored only when a class beyond the locked ones is switched on; otherwise the default applies. Appears once your account is linked to the Zuse Bifrost bot from the card above the grid; [Telegram](/integrations/telegram) walks through the code. On for every class by default. Switching everything off keeps the column and sends nothing to the chat. ## Emails you cannot opt out of A few emails go out regardless of the grid because they are how you get into, or back into, your account: the invitation that creates it, verification links when you change your address, password reset links, and the three critical classes above. ## What happens next Every alert writes one entry in your Notifications Center as it is sent, whatever else it does. The entry carries the alert's title, one line of context, its class and the page it points at, so an alert from a year ago reads like one from today. The same inbox and the same grid are readable by integrations through the REST API and by agents over MCP as those surfaces land. ## Permissions | Action | Who may do it | | ------------------------------ | ------------------------ | | Read and mark your alerts | You, on your own account | | Change your alert destinations | You, on your own account | ## Frequently asked It was one of the three critical classes, which are always mailed, or no other destination carried the class, so email was the fallback. Everything but the critical classes and the account emails, once your chat is linked and carries the rest. Until then email is the only place alerts can go. It appears once your account is linked to the bot. Press **Link Telegram** on the card above the grid and send the bot the code; see [Telegram](/integrations/telegram). No. It only clears it from the unread count; the thing the alert was about is unchanged. ## Related - [API tokens](/workspace/api-tokens): the first alert the Security class sends. - [Profile](/account/profile): change the email address receiving these alerts. - [Signing in](/account/sign-in): the account emails that bypass the grid. --- # Passkeys Source: https://alm-docs.labs.zuselab.dev/account/passkeys By the end of this page you will have a passkey registered on the device you are using, and you will know how to add one per device and remove one you no longer hold. A **passkey** is a credential kept by your device or your password manager and bound to this application. You unlock it with a fingerprint, a face scan or the device passcode, and it signs you in with nothing typed. ## Why passkeys A passkey answers only the web address it was registered for. A fake sign-in page gets nothing. No password to type, save or rotate. The device lock is the only thing between you and your account. A passkey sign-in skips the two-factor challenge, because the device unlock already proved it was you. No code to receive. It works on a plane and on a flaky connection. ## Before you begin | You need | Why | | --------------------------- | ---------------------------------------------------------------------------------------------------------------- | | A recent device and browser | Passkeys need the WebAuthn standard: current iOS, macOS, Android, Windows with Windows Hello, or a hardware key. | | A signed-in session | Passkeys are added from your own security settings. | A passkey kept in iCloud Keychain, Google Password Manager or a password manager such as 1Password syncs to your other devices signed in to the same account. One kept on a hardware key or an older system stays on that device. ## Register a passkey [step] 1. Open **Settings → Security** and find the **Passkeys** card. 2. Press **Create a New Passkey**. 3. Name it after the device or manager that will hold it, such as _Work laptop_ or _Phone_, so you can tell them apart later. 4. Press **Generate Passkey**. Your browser or operating system takes over with its own prompt and asks where to store the passkey. 5. Approve with your fingerprint, face or passcode. The prompt always comes from your device, never from the page. 6. The passkey appears in the list with its name and **Not used yet**. Register one on every device you sign in from, or keep one in a password manager that follows you. With two passkeys you are never locked out when one device is not to hand. ## Sign in with a passkey Open the sign-in page. Your browser offers the passkey as soon as you focus the email field; pick it and unlock. The flow, and what to do when the browser does not offer it, is on [Signing in](/account/sign-in#sign-in-with-a-passkey). ## Managing The list on the **Passkeys** card shows every passkey of yours with its name and when it last signed you in. ### Remove a passkey [step] Remove a passkey when the device that held it is sold, lost or wiped. 1. Press **Delete** beside the passkey. 2. Confirm it is you: type your password, or, on an account without a password, press **Confirm with a passkey** and unlock one of your other passkeys. 3. Press **Delete Passkey**. Removing a passkey here invalidates it: a sign-in with it is refused from now on. The credential may still exist in the device's keychain, which is normal; clean it up from the device's own settings. ### Replacing a device Register a passkey on the new device first, confirm it signs you in, then remove the old one. Removing first leaves you with the password alone until the new passkey is in place. ## Accounts without a password An invitation can be accepted with a passkey alone, which leaves the account with no password. Such an account signs in with its passkey, and every place that would ask for the current password asks for a passkey instead: setting a password, removing a passkey, signing other browsers out. If you hold no passkey either, use **Forgot your password?** on the sign-in page; the emailed link lets you set one. ## Permissions | Action | Who may do it | | ---------------------------- | ------------------------ | | Register or remove a passkey | You, on your own account | ## Frequently asked Sign in with your password and register a passkey on the new device. If the account has no password, use **Forgot your password?** to set one from an emailed link, then register the new passkey. Yes. A passkey is added to your ways in; it replaces nothing. The password and the two-factor code keep working. If it syncs through iCloud Keychain, Google Password Manager or a password manager, yes, with no extra setup. A device-bound passkey does not; register a new one. In practice yes. A passkey cannot be phished, reused or taken from a breach; a code can be typed into a convincing fake page. ## Related - [Two-factor authentication](/account/two-factor-auth): the code-based second factor. - [Signing in](/account/sign-in): the passkey prompt on the sign-in page. - [Browser sessions](/account/sessions): sign out the devices you no longer hold. --- # Changing Your Password Source: https://alm-docs.labs.zuselab.dev/account/password By the end of this page you will have set a new password and know what the change does and does not touch. Your password is one of the ways you sign in; the others are a [passkey](/account/passkeys) and, behind either, a [second factor](/account/two-factor-auth). The form to change it sits at the top of **Account Settings → Security**, above the browser sessions, the two-factor card and the passkeys. ## Before you begin | You need | Why | | ------------------------------------------ | ------------------------------------------------------------------------------------------------- | | Your current password, or a passkey | The form asks for one of them before it writes the new password, so a left-open session cannot change it. | | A new password of at least eight characters | Shorter ones are refused; a password manager's generated string is the best choice. | ## Change your password [step] 1. Open **Account Settings → Security**. 2. Under **Update password**, type your **Current password**. An account that holds no password confirms with a passkey instead, and the field is not shown. 3. Type the **New password** and the same again under **Confirm password**. 4. Press **Save**. A toast confirms the change, and the activity log records that your password was updated, never the value. Your other browser sessions stay signed in; sign them out from the sessions card beneath the form if the reason for the change was a device you no longer trust ([Browser sessions](/account/sessions)). An account created by accepting an invitation with a passkey has no password. Such an account can set one here, confirming with the passkey, or keep signing in with the passkey and never set one. If you hold neither, use **Forgot your password?** on the sign-in page; the emailed link lets you set one ([Signing in](/account/sign-in#forgot-your-password)). ## All the fields Required when the account has a password. Checked against the stored hash before anything changes; a wrong one is refused on the field. Not shown for an account that signs in with a passkey alone, which confirms with the passkey instead. Required, at least eight characters. Stored as a hash the moment you save; nothing can show it again. Required and must match the new password exactly, so a mistyped password is caught before it is stored. ## What happens next - The new password works from your next sign-in. Nothing is emailed; a sign-in from a new device is still announced the way your [notification destinations](/account/notifications) say. - Your passkeys, your second factor and your recovery codes are untouched. - Your other browser sessions are untouched. Sign them out yourself from the card below the form. ## Permissions | Action | Permission | | -------------------- | ----------------------------------- | | Change your password | Being signed in; no role is needed. | ## Frequently asked Sign out and use **Forgot your password?** on the sign-in page. The emailed link lets you set a new one without the old; it expires after an hour. No. Nobody can set your password for you; an admin can only remove you from a team. If you are locked out, the reset link is the way back in. ## Related - [Signing in](/account/sign-in) — the password reset flow - [Passkeys](/account/passkeys) — signing in without a password - [Two-factor authentication](/account/two-factor-auth) — the second step behind either - [Browser sessions](/account/sessions) — signing other devices out --- # Profile Source: https://alm-docs.labs.zuselab.dev/account/profile By the end of this page you will know what the profile form changes, who sees each field, and what happens when you change the address you sign in with. Profile settings live under **Settings → Profile**. Open the account menu at the bottom of the sidebar, choose **Account Settings**, and the profile page opens first. ## Before you begin | You need | Why | | -------------------- | ----------------------------------------------------------------------------------- | | A signed-in session | Every settings page is yours alone; nobody edits another person's profile. | | Access to your inbox | A changed email address must be verified before the verified-only pages open again. | ## Update your details [step] 1. Open **Settings → Profile**. 2. Change your **Name**, **Email** or **Job title**. 3. Press **Save Changes**. A short confirmation appears beneath the button and the change applies at once across the application. ## All the fields Your display name: it appears in the sidebar, on every activity entry you cause and beside every comment, approval and time log of yours. Two characters at least. The address you sign in with and the one every notification goes to. Capitalisation does not matter; the address is stored lowercased. It must not belong to another account in the workspace. Changing it starts the verification described below. Optional. Shown beside your name to the people you work with, so a client reading a comment knows whether it came from the analyst or the developer. Leave it blank to show no title. ## What happens when you change your email address Your email address is also your sign-in identifier, so a new address is treated as unproven until you click a link sent to it. 1. The new address is saved immediately and a verification email goes to it. 2. Your account is marked unverified. You stay signed in, but the pages that require a verified address send you to the verification notice until the link is clicked. The settings pages stay open so you can correct a typo. 3. Open the email at the **new** address and follow the link. It lives for a day and works once; an expired link lands on a page that sends a fresh one. 4. Sign-in now expects the new address. The old one no longer works. If the verification email does not arrive, check the spam folder of the new address, then press **Click here to re-send the verification email** beneath the Email field. One email a minute is sent per address. ## Managing There is no avatar upload yet; the application shows your initials. Deleting an account is a workspace admin's action, because projects, approvals and time logs stay attributed to the person who made them. ## Permissions | Action | Who may do it | | --------------------- | ------------------------ | | Edit your profile | You, on your own account | | Deactivate an account | A workspace admin | ## Frequently asked Activity entries name the account, not a copy of the name at the time, so a rename shows everywhere at once. Nothing you did is lost or reattributed. No. One address, one account. The form refuses an address another account holds, in any capitalisation. The password and the second factor are under [Security](/account/two-factor-auth); language and timezone under [Timezone and language](/account/timezone-and-locale). ## Related - [Signing in](/account/sign-in): what the address is used for and how verification works. - [Timezone and language](/account/timezone-and-locale): the other personal settings. --- # Browser Sessions Source: https://alm-docs.labs.zuselab.dev/account/sessions By the end of this page you will know how to review the browsers signed in to your account and how to sign out every one of them but the one you are using. The **Browser Sessions** card lives under **Settings → Security**, beneath the password card. The card appears when your workspace keeps a record of signed-in browsers. If you do not see it, the list is not available in your workspace and a password reset remains the way to sign remembered browsers out. ## What the list shows | Field | What it tells you | | ---------------------- | ---------------------------------------------------------------------------------------------- | | **Device and browser** | The operating system and browser that signed in, such as _Windows - Chrome_ or _iOS - Safari_. | | **IP address** | The address the session last connected from, useful for spotting an unfamiliar place. | | **Last active** | How long ago the session made a request. | | **This device** | Marks the session you are reading the page on, so you do not try to revoke it. | ## Sign the other browsers out [step] 1. On the **Browser Sessions** card, press **Log Out Other Browser Sessions**. 2. Confirm it is you: type your password, or, on an account without a password, press **Confirm with a passkey** and unlock one. 3. Press **Log Out Other Browser Sessions** in the dialog. 4. Every session except this one is ended. Those browsers are asked to sign in again on their next request, and the list refreshes to show only this device. The action is all or nothing; a single session cannot be picked out. If one device worries you, sign out everywhere, then change your password so the credentials that device holds stop working too. ## When to use it - **A device left your hands.** Sold, given away, lost: sign the others out, then change your password. - **A shared or public computer.** When you could not confirm the sign-out took effect, clear the slate from your own device. - **An unfamiliar address or place in the list.** Treat it as a compromise: sign out, change the password, regenerate your recovery codes. - **After a password reset.** Resetting from an emailed link already signs remembered browsers out; this card confirms it. - **Routine hygiene.** A short list is easier to read than a long one. ## What happens next The ended sessions receive no message. Their next request lands on the sign-in page, their **Remember Me** no longer applies, and nothing can bring a revoked session back. Your own session continues unchanged. ## Permissions | Action | Who may do it | | ---------------------------- | ------------------------ | | Review and end your sessions | You, on your own account | ## Frequently asked Sign the others out, change your password, and regenerate your recovery codes if two-factor authentication is on. Then review your passkeys and remove any you do not recognise. The address is the one your connection presents, which is often a carrier or office gateway elsewhere in the country. The browser and operating system are the better clue. No. Those use tokens, not a browser session; they are managed from the workspace's token settings. ## Related - [Signing in](/account/sign-in): Remember Me and what a password reset signs out. - [Two-factor authentication](/account/two-factor-auth): the code that stops a stolen password from signing in. - [Passkeys](/account/passkeys): remove the passkey of a device you no longer hold. --- # Signing In Source: https://alm-docs.labs.zuselab.dev/account/sign-in By the end of this page you will know every way into your account, what the sign-in page refuses and why, and how to get back in when a password or an emailed link has stopped working. ## Before you begin | You need | Why | | --------------------------- | -------------------------------------------------------------------------------------------------------------------- | | An accepted invitation | Accounts are created by invitation only; the sign-in page has no sign-up link. See [Your account](/account). | | A verified address | The sign-in page refuses an address that has not been verified yet, and offers to send the verification email again. | | Your password, or a passkey | A passkey registered on the device you are using signs you in without a password at all. | ## Sign in with your password [step] 1. Open the sign-in page. It is where every link into the application sends you while you are signed out. 2. Enter your **email address**. Capitalisation and surrounding spaces do not matter; `Nimal@Example.com` and `nimal@example.com` are the same account. 3. Enter your **password**. The eye icon shows what you typed. 4. Tick **Remember Me** if the device is yours and you want to stay signed in after closing the browser. 5. Press **Log in to Zuse Bifrost**. A wrong address or a wrong password gets the same answer, so the page never confirms which addresses have accounts. After five failed attempts for the same address from the same connection, the page locks that address for a minute and says how long to wait; a correct password does not get through during the lockout. Where you land next depends on your account: - With no second factor, you arrive on the **Dashboard**, or on the page you were trying to open before you were sent to sign in. - With two-factor authentication turned on, you are taken to the [two-factor challenge](#answer-the-two-factor-challenge) first. ## Sign in with a passkey [step] A passkey is a credential stored on your device or in your password manager that signs you in with a fingerprint, a face scan or the device's screen lock. Nothing is typed and nothing can be phished. 1. Open the sign-in page. If your browser holds a passkey for Zuse Bifrost, it offers it as soon as you focus the email field, the way it offers a saved password. 2. Pick the passkey. You may have one per device. 3. Unlock it with your biometric or your screen lock. You are signed in straight away. A passkey already proves the device and the person holding it, so the two-factor challenge is not asked on top of it. A passkey works only on a device or password manager that holds it, and only on the same web address it was registered against. On a new device, sign in with your password instead. ## Answer the two-factor challenge [step] When two-factor authentication is on for your account, a correct password is only the first step. Nobody is signed in yet; the page waits for a code. 1. Open your authenticator app and find the Zuse Bifrost entry. 2. Type the six-digit **authentication code**. The page submits on its own as soon as the sixth digit is in; **Continue** does the same by hand. Pasting a code fills all six boxes at once. 3. You are signed in, and your **Remember Me** choice from the password step is honoured. If you cannot reach your authenticator, choose **login using a recovery code** and paste one of the recovery codes you saved when you turned the second factor on. A recovery code works once and is then spent; the rest remain valid. A code followed by other text is not accepted, so paste the code alone. Five wrong codes in a row lock the challenge for a minute. The lock is per account, not per connection, because whoever is guessing already holds the password. The challenge is only a parked sign-in. Closing the browser abandons it, and signing in again starts from the password. It never blocks an account that is already signed in elsewhere. ## Stay signed in **Remember Me** keeps your session alive after you close the browser, on that device, until you sign out or your password changes. Leave it unticked on a shared computer. Resetting your password from an emailed link signs every remembered browser out at once, which is the quickest way to shut a device out when you no longer hold it. ## If you forgot your password [step] 1. On the sign-in page, follow **Forgot your password?**. 2. Enter your email address and press **Email Password Reset Link**. 3. The page answers _A reset link will be sent if the account exists_, whatever you typed. It says the same thing for an address nobody registered, so it cannot be used to find out who has an account. 4. Open the email and follow the link. It opens the **Reset Password** page with your address filled in. 5. Choose a new password, confirm it, and press **Reset Password**. You are sent to the sign-in page with a confirmation and can sign in with the new password straight away. A reset link works for one hour and once. An expired or already-used link is refused on the reset page with the reason, and you simply ask for another. Five requests a minute from one connection are accepted; after that the page asks you to wait. ## If your address is not verified yet Until the address on your account is verified, the sign-in page refuses it even with the right password and says so. Beneath the message it offers **Send me a new verification email**; press it and a fresh link goes to your inbox. One link a minute is sent per address, and the page tells you how long to wait if you ask again too soon. A verification link lives for a day and works once. A link that has expired, or that was altered on its way to you, lands on the **This verification link has expired** page, where you type your address and receive a fresh one. That page answers in the same words whether or not the address has an unverified account, so it tells a stranger nothing. Following a valid link verifies the address and signs you in, or takes you to the two-factor challenge when your account has one. A link for an address that is already verified changes nothing and signs nobody in: it proves an inbox, not a password. ## If your account was deactivated A workspace admin can deactivate an account when someone leaves. The sign-in page then refuses the right password with _This account has been deactivated. Ask a workspace admin to restore it._; a passkey and an emailed link are refused the same way. Your history stays attached to the account, and an admin can restore it. ## Signing out Open the account menu at the bottom of the sidebar (top right on a phone) and choose **Log Out**. Signing out ends the session on that device only; other devices you ticked **Remember Me** on stay signed in until you sign out there or reset your password. A sign-out link that reaches you from outside the application, such as one in an email, opens a page that signs you out as it loads. If the link was planted on another site, the page shows a button instead and waits for you, so nobody can end your session by making you follow a link. ## Frequently asked Five wrong passwords for the same address from the same connection lock it for a minute. Wait it out, then use **Forgot your password?** rather than guessing further. Codes depend on the clock of the phone and the clock of the server being within thirty seconds of each other. Set the phone to take its time from the network, then try the next code. A recovery code gets you in meanwhile. Ask a workspace admin. They can turn the second factor off on your account after confirming it is you, and you set it up again on the new device. The passkey is offered only on the device or password manager that holds it, and only when the browser supports passkey autofill. Sign in with your password on this device. You were not. After a successful sign-in, and after the two-factor challenge, you are returned to the page you were trying to open. ## Related - [Two-factor authentication](/account/two-factor-auth): turning the code on, recovery codes, moving phones. - [Passkeys](/account/passkeys): registering and removing passkeys. - [Browser sessions](/account/sessions): which browsers are signed in, and signing the others out. - [Your account](/account): how accounts are created and why they are personal. --- # Timezone and Language Source: https://alm-docs.labs.zuselab.dev/account/timezone-and-locale By the end of this page you will know where your account's timezone came from, how to confirm or change it, and where the language setting lives. Both settings live under **Settings → Localization**, as two independent cards with their own **Save Changes** buttons. ## Language The **Language Selection** card lists the languages the application speaks. One language ships today, English, so the picker has one option; the card exists so the choice is already on your account when another language arrives. Pick the language and press **Save Changes**. ## Where your timezone comes from Every time you enter and every time you read, from sprint boundaries to maintenance windows and scheduled reports, is read in your account's timezone. A wrong value never contradicts itself: every screen agrees, and the sprint simply starts at the wrong hour. So the application takes care to know how the value was arrived at, and the **Timezone Settings** card tells you which of three states you are in. | What the card says | What it means | | -------------------------------- | ------------------------------------------------------------------------------------- | | "This is the timezone you chose" | You picked it, here or by answering a suggestion. Nothing changes it but you. | | "Detected from your browser" | The application read it from your browser. A later detection may revise it. | | "You have never set this" | Neither happened; the card shows the workspace default rather than a choice of yours. | ### Detection The first time your browser opens the application in a session, it reports its timezone. If your account has no chosen zone, the report is stored silently as _detected_. If the report disagrees with a zone you have already scheduled work against, nothing changes: a banner at the top of the page shows both zones with their current offsets and offers **Use** the detected one or **Keep** the current one. Either answer counts as a choice and the banner does not return. Whatever you choose, anything already scheduled stays exactly where it is. Switching changes only the times you enter from then on. ## Change your timezone [step] 1. Open **Settings → Localization** and find the **Timezone Settings** card. 2. Pick your zone from the list. It is grouped by region and searchable; typing a city such as _Colombo_ or _London_ jumps to it. Each entry shows its current offset. 3. Press **Save Changes**. 4. Every time in the application is now shown in the chosen zone, and the card reads "This is the timezone you chose". Choosing the zone already shown counts too: it turns a default into a decision. A chosen zone is never overwritten by detection. Travelling does not move it, which keeps sprint reports consistent across trips; change it by hand when you relocate for good. Running on a zone that is not your own is a legitimate choice for someone who plans for a team elsewhere. ## Where the timezone is used - **Dates and times on every page**: activity entries, due dates, last-used stamps and the like are shown in your zone. - **Times you enter**: a sprint that starts "Monday 09:00" starts at 09:00 in your zone. - **Emails and digests**: times in notifications are written in your zone. ## Permissions | Action | Who may do it | | --------------------------------- | ------------------------ | | Choose your language and timezone | You, on your own account | ## Frequently asked Press **Keep**. The zone becomes a confirmed choice and detection leaves it alone from then on. Search by the nearest large city in the same zone; the list uses the standard names, such as `Asia/Colombo` or `America/New_York`. Older spellings the browser may report, such as `Asia/Calcutta`, are recognised and stored under their current name. Theirs. Each person reads the same instant in their own zone, so a sprint boundary set by a planner in Colombo shows a developer in London the equivalent local time. ## Related - [Profile](/account/profile): your name, email address and job title. - [Appearance](/account/appearance): light and dark mode. --- # Two-Factor Authentication Source: https://alm-docs.labs.zuselab.dev/account/two-factor-auth By the end of this page you will have two-factor authentication turned on, your recovery codes saved, and a plan for the day you lose the phone. **Two-factor authentication** means that after your password, you are asked for a six-digit code that only your authenticator app can produce. Someone who learns your password still cannot sign in without your device. A [passkey](/account/passkeys) is the other way to add a second factor, and for daily use it is faster: one unlock replaces the password and the code. Both can be on at once; a passkey sign-in skips the code because the device unlock already proved it was you. ## Before you begin | You need | Why | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------- | | An authenticator app | Any app that generates time-based codes: Google Authenticator, Microsoft Authenticator, Authy, 1Password, Bitwarden. | | A password on your account | The code is asked after the password. An account that signs in with a passkey alone does not need a code. | | A safe place for eight codes | A password manager or a printed sheet; the recovery codes are shown once. | ## Turn it on [step] 1. Open **Settings → Security** and find the **Two Factor Authentication (2FA)** card. It reads **Disabled**. 2. Press **Enable Two Factor Authentication**. A dialog opens with a QR code. 3. In your authenticator app, add an account by scanning the code. If you cannot scan, copy the key beneath the code and enter it by hand; the app lists the entry under the application's name and your email address. 4. Press **Continue**, type the six-digit code your app shows, and press **Confirm**. The code boxes submit on their own as soon as the sixth digit is in. 5. The card now reads **Enabled** and shows your recovery codes, expanded. Save them now. Until the code is confirmed nothing changes: the enrolment is pending, your password still signs you in alone, and closing the dialog with **Cancel** forgets the secret. Reloading the page mid-enrolment keeps it, so you can scan at your own pace. ## Recovery codes Enabling the factor mints eight single-use recovery codes, each shaped `XXXXXXXXXX-XXXXXXXXXX`. A recovery code stands in for an authenticator code once; after it is used it is spent, and the rest remain valid. Store the codes before you leave the page. They are shown expanded right after enrolment and collapsed on later visits; you can view them again any time, but nothing reminds you to. On the **Enabled** card: - **View My Recovery Codes** expands the list; **Hide Recovery Codes** collapses it. The line beneath counts how many remain. - **Regenerate Codes** replaces all eight at once, used or not. Do this when you suspect a code leaked, or when you are down to one or two. ## Signing in with the code After a correct password you are taken to the two-factor challenge. Type the code from your app, or choose **login using a recovery code** and paste one. Five wrong codes lock the challenge for a minute. The full flow is on [Signing in](/account/sign-in#answer-the-two-factor-challenge). ## Turn it off [step] 1. Open **Settings → Security**. 2. On the **Enabled** card, press **Disable Two Factor Authentication**. 3. The secret and every recovery code are removed at once. The next sign-in asks for the password alone. Turn it off only to move to a new authenticator or to a passkey. Set the replacement up first, then disable the old factor. ## Moving to a new phone Before wiping the old phone, transfer the authenticator entries with the app's own export feature, or sign in and re-enrol: turn the factor off, then on again, and scan the new QR code with the new phone. If the old phone is already gone, sign in with a recovery code and do the same. ## Permissions | Action | Who may do it | | ------------------------------------------ | ------------------------------------------------ | | Enable, disable, regenerate | You, on your own account | | Turn the factor off for someone locked out | A workspace admin, after confirming who they are | ## Frequently asked Codes depend on the phone's clock. Set the phone to take its time from the network and try the next code. If the enrolment was never confirmed, the card still reads Disabled; start again from Enable. Ask a workspace admin. They can turn the factor off on your account once they have confirmed it is you, and you enrol again on the new device. No. The passkey already proves the device and the person holding it, which is what the code exists to add to a password. No. Following a verification link verifies the address and then takes you to the challenge like any password sign-in would. ## Two-factor codes and passkeys | Feature | Authenticator code | Passkey | | --------------------- | ------------------------------------ | ----------------------------------------------------- | | Extra step at sign-in | Yes, after the password | No; the passkey replaces the password | | Needs a phone | Yes | Any device or password manager that holds the passkey | | Works offline | Yes | Yes | | Recovery | Eight recovery codes | Another passkey, or the password | | Phishing | A code can be typed into a fake page | Bound to this application; a fake page gets nothing | ## Related - [Passkeys](/account/passkeys): the faster second factor. - [Signing in](/account/sign-in): the challenge, lockouts and recovery codes at sign-in. - [Browser sessions](/account/sessions): sign other devices out after a scare. --- # Ability Catalogue Source: https://alm-docs.labs.zuselab.dev/api/v1/abilities Ability strings are the fine-grained permissions you tick when you mint a [personal access token](/api/v1/authentication). Miss the one an endpoint or tool needs and the request returns `TOKEN_MISSING_ABILITY`, naming it in `error.context.required_ability`. ## One string, every surface A single ability, say `project:create`, is the source of truth in three places: 1. **The app**, when a person presses "Create project": their role or group must carry the matching permission. 2. **The REST API**, in the route middleware before the controller runs. 3. **The MCP server**, inside the tool handler before the action runs. An MCP tool requires exactly what its REST equivalent requires. Mint for the operations you intend to perform, never for the channel you intend to perform them over. A client connected to the MCP server by signing in acts as the person themselves and satisfies every ability their team role grants; this catalogue governs what a personal access token carries. ## Format `:`, with `:` as the separator and `-` inside a multi-word entity or action: `work-item:view-any`, `change-request:approve`. Most entities carry the five CRUD actions (`view-any`, `view`, `create`, `update`, `delete`), and some add named actions for the moments in their lifecycle that are not CRUD: `epic:submit` and `epic:approve`, `sprint:start` and `sprint:close`, `release:freeze`, `release:publish` and `release:rollback`. Three entities carry no CRUD at all: - `team-member`: membership is `invite`d and `remove`d rather than created and deleted, and `update-role` is its own action. - `webhook-delivery`: a delivery is a record of something that already happened, so it can only be listed, viewed and `retry`'d. - `report`, `dashboard` and `activity`: read-only, each with the single action `read`. ## The catalogue The catalogue mirrors the [team permission catalogue](/workspace/roles) one to one: every permission a role can grant is an ability a token can carry, plus the five extras below that exist only for tokens and agents. A machine-readable copy is served by the API host at `/abilities.json` once the catalogue export lands, regenerated on every deploy. ### Families | Family | Entities | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Clients | `client` | | Projects | `project`, `project-member` | | Components and environments | `component`, `environment` (+ `deploy`, `freeze`), `repository` | | Project registry | `stakeholder`, `persona`, `glossary`, `data-entity`, `design-reference` | | Planning | `epic` (+ `submit`, `approve`), `work-item` (+ `transition`, `assign`, `rank`), `requirement` (+ `submit`, `approve`), `business-rule` (+ `submit`, `approve`), `use-case`, `raid`, `decision`, `link`, `baseline` (+ `freeze`), `approval` (`view-any`, `view`, `decide`, `withdraw`, `manage-policies`), `change-request` (+ `analyse`, `approve`, `verify`) | | Build | `sprint` (+ `start`, `close`), `time-log` (+ `approve`) | | Testing | `test-suite`, `test-case` (+ `approve`), `test-plan` (+ `approve`), `test-run` (+ `execute`, `sign-off`) | | Releases and deployments | `release` (+ `freeze`, `approve`, `publish`, `rollback`), `deployment` (+ `record`, `promote`) | | Maintenance | `incident` (+ `command`, `resolve`, `publish-update`), `ticket` (+ `respond`, `convert`), `maintenance-window` | | Knowledge and documents | `knowledge`, `document` (+ `render`, `finalise`), `import` | | Attachments | `attachment` | | Comments | `comment` (+ `internal`) | | Reports and dashboards | `report:read`, `dashboard:read` | | Activity log | `activity:read` | | AI | `ai:use`, `ai:manage-budgets` | | Teams | `team`, `team-member` (`view-any`, `view`, `invite`, `remove`, `update-role`) | | Roles and groups | `role`, `group` | | API tokens | `token` | | Webhooks | `webhook-endpoint`, `webhook-delivery` (`view-any`, `view`, `retry`) | | Integrations | `integration:manage` | ### Extras Five abilities exist for tokens and agents only, outside the team permission catalogue: | Ability | What it opens | | ----------------- | ----------------------------------------------------------------------------------------------------- | | `account:read` | `GET /v1/me`, the caller's notifications and alert destinations. | | `account:write` | Marking the caller's notifications read and replacing their alert destinations. | | `mcp:use` | The OAuth scope an MCP client is granted when a person signs in; it stands for their whole catalogue. | | `ci:deploy` | A pipeline token that records deployments and nothing else. | | `ci:test-results` | A pipeline token that reports test runs and nothing else. | ## Enforced today The REST API ships family by family as each phase of the product lands. Today these families have live endpoints, and every other ability is accepted on a token and gates nothing yet: | Family | Endpoints | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Account | `GET /v1/me`, `GET /v1/me/notifications`, the two marks, `GET /v1/me/alert-destinations` and its `PUT`. | | Teams | `GET /v1/teams`, `/teams/current`, `/teams/{team}`, create, rename and delete; the roster, one member, the invitations, inviting, re-roling, removing and withdrawing. | | Roles and groups | List, read, create, update and delete; `PUT /v1/groups/{group}/members`. | | API tokens | List, read, mint and revoke. | | Webhooks | Endpoints: list, read, register, pause, resume, rotate, test, delete. Deliveries: list, read, retry, retry dead. Event samples. | | Activity log | `GET /v1/activity` for a project, a client or a project member. | | Projects | `GET /v1/projects` and `GET /v1/projects/{project}`, read-only until the registry lands. | `ai:use` gates one kind of REST route: asking Heimdall for a [quality review](/planning/quality-review) under a work item, a requirement or a business rule (`POST …/quality-reviews`), and the `review_quality` tool that does the same. Everything else Heimdall does runs in-app, and an outside agent drives Zuse Bifrost through the MCP server. ## Choosing abilities when minting Least privilege, in practice: 1. **Read-only tooling**: the `:view-any` and `:view` abilities for the entities you read, and nothing else. 2. **A pipeline**: `ci:deploy` or `ci:test-results`, confined to one project with a `scope:project:` entry. 3. **An assistant over MCP**: every ability for the tools you want it to run. Start with reads; the tools reference maps each tool to its ability. 4. **A token that mints other tokens**: add `token:create`, and read attenuation first. A token can only grant what it already holds, and can never pass on `token:create`. ## Team and project scope entries Alongside abilities, every token carries one required and zero-to-many optional scope entries: - `scope:team:`, **required**. Frozen at mint time; the token can never act outside this team. - `scope:project:`, **optional, repeatable**. If present, the token is restricted to the listed project ids even when the person may see more projects inside the team. A guest's token always carries one. These are stored in the same array as abilities but are not abilities, and the tokens endpoints report them separately under `scopes` for that reason. See [tenancy and scopes](/api/v1/tenancy-and-scopes) for how they combine with tenant isolation at request time. --- # Agent Readiness Source: https://alm-docs.labs.zuselab.dev/api/v1/agent-readiness Zuse Bifrost plugs into Claude, ChatGPT, Cursor and VS Code, so an agent can read a project's record and write it back, as far as the person's role allows, instead of a person clicking through the screens. Connecting takes about two minutes: start with the [quickstart](/mcp/v1/quickstart), or pick the guide for your app, [Claude Desktop](/mcp/v1/connecting-claude-desktop), [Cursor](/mcp/v1/connecting-cursor), [ChatGPT](/mcp/v1/connecting-chatgpt-desktop) or [VS Code](/mcp/v1/connecting-vscode-claude), then read the [prompting guide](/mcp/v1/prompting-guide) to see what to say. Zuse Bifrost publishes a small set of well-known documents so that a client, a crawler or an AI agent that knows nothing about Zuse Bifrost can fetch one URL and learn which APIs exist, where each one's machine-readable description is, how to authenticate, where its documentation lives and where to check that it is up. Every document is built from the running application's configuration, the MCP server's own declarations and the tool catalogue, so none of them can name a host, a version or a count that the deployment does not have. The addresses below are written as `https://app.example.test` for the application, `https://api.example.test` for the API host and `https://mcp.example.test` for the MCP server; your workspace's addresses are the ones your administrator shares. | Document | Standard | What it answers | | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `https://app.example.test/.well-known/api-catalog` | [RFC 9727](https://www.rfc-editor.org/rfc/rfc9727) | Which APIs exist and where their specification, documentation and health endpoints are. | | `https://app.example.test/.well-known/mcp/server-card.json` | [MCP server card](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1649) (SEP-1649) | Where the MCP server is, the protocol versions it speaks, its OAuth metadata and its capabilities. | | `https://app.example.test/.well-known/agent-skills/index.json` | [Agent Skills Discovery](https://agentskills.io/) 0.2.0 | The instruction files an agent loads to work with the REST API, the MCP server and webhooks. | | `https://app.example.test/.well-known/openid-configuration` | [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) path, RFC 8414 body | The OAuth 2.0 authorization-server metadata, for clients that probe only that path. | | `https://app.example.test/AGENTS.md` | [AGENTS.md](https://agents.md) | The briefing for an agent: the surfaces, the two ways to a credential, the shared rules, what needs a human, and where everything else is. | | `https://app.example.test/llms.txt` | [llms.txt](https://llmstxt.org) | Where this documentation describes the product for language models, and where the machine-readable documents are. | | This documentation's [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt) | llms.txt | Every page of this documentation with a one-line summary, and the text of every page in one file. | All of them answer `GET`, carry `Cache-Control: public, max-age=3600`, need no credentials and may be read from any origin (`Access-Control-Allow-Origin: *`), so a validator or an agent running in a browser can fetch them. ## API catalog The catalog is the discovery document defined by RFC 9727. It lives on the application's address and is answered on the API host as well, for clients that start there: | URL | Purpose | | -------------------------------------------------- | ----------------------------------------------------------- | | `https://app.example.test/.well-known/api-catalog` | The catalog, on the application's address. | | `https://api.example.test/.well-known/api-catalog` | The same catalog, for clients that start from the API host. | The response is served as `application/linkset+json` with the RFC 9727 profile parameter and may be cached for an hour. ```bash curl -s https://app.example.test/.well-known/api-catalog | jq ``` ### The document The catalog is a [Linkset](https://www.rfc-editor.org/rfc/rfc9264): one entry per API, each anchored on the URL that represents the API and carrying typed links. ```json { "linkset": [ { "anchor": "https://api.example.test/v1", "service-desc": [ { "href": "https://api.example.test/openapi.json", "type": "application/vnd.oai.openapi+json" } ], "service-doc": [ { "href": "https://docs.example.test/api/v1", "type": "text/html" }, { "href": "https://docs.example.test/webhooks/v1", "type": "text/html" } ], "status": [{ "href": "https://api.example.test/up" }] }, { "anchor": "https://mcp.example.test", "service-doc": [ { "href": "https://docs.example.test/mcp/v1", "type": "text/html" } ], "service-meta": [ { "href": "https://mcp.example.test/.well-known/oauth-protected-resource", "type": "application/json" }, { "href": "https://app.example.test/.well-known/mcp/server-card.json", "type": "application/json" } ], "status": [{ "href": "https://app.example.test/up" }] } ] } ``` ### What each relation points to | Relation | REST API (`https://api.example.test/v1`) | MCP server (`https://mcp.example.test`) | | -------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `service-desc` | The [OpenAPI 3.1 document](/api/v1/openapi) describing every `/v1/*` operation and every webhook event. | None: an MCP server describes its own tools and resources to a connected client over the protocol. | | `service-doc` | This [REST API guide](/api/v1) and the [Outbound Webhooks guide](/webhooks/v1). | The [MCP server guide](/mcp/v1). | | `service-meta` | None. | The OAuth protected-resource metadata a client reads to find the authorization server and the scope, and the server card. | | `status` | The health endpoint, which answers `200` while the service is up. | The application's health endpoint, which also answers for the MCP server it hosts. | ## MCP server card The server card follows the shape proposed for MCP discovery in [SEP-1649](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1649), and names the server at its top level (`name` in the registry's reverse-DNS form, `title`, `description`, `version`, `websiteUrl`) the way a registry listing does. It is deliberately small: identity, transport, protocol versions, authentication and capability flags with counts. The tool catalogue itself is linked (`toolCatalogUrl`), not copied, because the server describes its tools to a connected client over the protocol. It is served on the application's address and, when the MCP server has an address of its own, there as well: | URL | | ----------------------------------------------------------- | | `https://app.example.test/.well-known/mcp/server-card.json` | | `https://mcp.example.test/.well-known/mcp/server-card.json` | ```json { "name": "test.example.app/mcp", "title": "Zuse Bifrost MCP Server", "description": "…", "version": "1.0.0", "websiteUrl": "https://app.example.test", "serverInfo": { "name": "Zuse Bifrost", "title": "Zuse Bifrost MCP Server", "version": "1.0.0", "description": "…", "websiteUrl": "https://app.example.test" }, "protocolVersion": "2025-06-18", "supportedProtocolVersions": ["2025-06-18", "2025-03-26"], "transport": { "type": "streamable-http", "endpoint": "https://mcp.example.test" }, "remotes": [{ "type": "streamable-http", "url": "https://mcp.example.test" }], "authentication": { "schemes": ["oauth2", "bearer"], "resourceMetadataUrl": "https://mcp.example.test/.well-known/oauth-protected-resource", "authorizationServers": ["https://app.example.test"], "scopes": ["mcp:use"] }, "capabilities": { "tools": { "listChanged": false, "count": 26 }, "resources": { "listChanged": false, "count": 3 }, "prompts": { "listChanged": false, "count": 0 } }, "documentationUrl": "https://docs.example.test/mcp/v1", "toolCatalogUrl": "https://api.example.test/mcp-tools.json" } ``` Every field is read from the server class, the package and the catalogue: the protocol versions are the ones the server negotiates, the `listChanged` flags are the ones it declares, and the counts are the live tool and resource registrations, so the card says exactly what the `initialize` handshake would. The versions and counts above are illustrative; read the card for the current ones. ## Agent skills A skill is a short instruction file, `SKILL.md`, that an agent loads when it needs to work with Zuse Bifrost. The index follows the [Agent Skills Discovery](https://agentskills.io/) RFC 0.2.0 and lists three skills: | Skill | File | What it teaches | | --------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `zusegenius-rest-api` | `https://app.example.test/.well-known/agent-skills/zusegenius-rest-api/SKILL.md` | Tokens and abilities, how lists page, the `Idempotency-Key` rule on every write, the error envelope and the rate limits, and where the OpenAPI document is. | | `zusegenius-mcp` | `https://app.example.test/.well-known/agent-skills/zusegenius-mcp/SKILL.md` | The endpoint, OAuth 2.1 against a personal access token, the first calls to make, how to read the tool listing and the rule on tools flagged destructive. | | `zusegenius-webhooks` | `https://app.example.test/.well-known/agent-skills/zusegenius-webhooks/SKILL.md` | Registering an endpoint, the `ZG-Signature`, `ZG-Event-Id` and `ZG-Event-Name` headers, verifying the HMAC over the raw body, deduplicating, and the retry schedule. | ```json { "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", "skills": [ { "name": "zusegenius-rest-api", "type": "skill-md", "description": "Call the Zuse Bifrost REST API: authenticate with a personal access token, page through lists, send an Idempotency-Key on every write, honour the rate limits and read the OpenAPI document.", "url": "https://app.example.test/.well-known/agent-skills/zusegenius-rest-api/SKILL.md", "digest": "sha256:…" } ] } ``` Each file opens with YAML front matter carrying the same `name` and `description` as the index, followed by Markdown instructions. The `digest` is the SHA-256 of the exact bytes the file's URL serves, so a client can check that what it loaded is what the index promised. The instructions are generated from the application's configuration and the constants that enforce each rule: the hosts, the page-size ceiling, the idempotency window, the rate limits, the webhook retry schedule and the tool and event counts are the running values, never typed into the text. ```bash curl -s https://app.example.test/.well-known/agent-skills/zusegenius-mcp/SKILL.md ``` ## OpenID Connect discovery alias Some clients look for an authorization server's metadata only at the OpenID Connect discovery path. The authorization server (the application's address) therefore answers `/.well-known/openid-configuration` with the same [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) document it serves at `/.well-known/oauth-authorization-server`: the issuer, the authorization, token and dynamic-registration endpoints, the `code` response type, `S256` as the only PKCE method, the `mcp:use` scope and the `authorization_code` and `refresh_token` grants. It is an OAuth 2.0 document at the OpenID Connect path, not an OpenID provider: there is no `jwks_uri` and no ID token. Both metadata documents and the dynamic registration endpoint can be called from a client running in a browser. See [MCP authentication](/mcp/v1/authentication) for the flow. ## AGENTS.md `https://app.example.test/AGENTS.md` (also `/agents.md` and `/.well-known/AGENTS.md`) is the briefing an agent reads first: what Zuse Bifrost is as a service, the two ways to a credential, the REST API, the MCP server and the webhooks in a paragraph each, the conventions every surface shares, the operations that need a human before they run, what never to do, and where every longer document is. Like the skills, every address, count and limit in it is read from the running application. ```bash curl -s https://app.example.test/AGENTS.md ``` ## llms.txt The application serves `https://app.example.test/llms.txt` so a crawler that starts at the application's address finds the product described for language models: it points at this documentation's [`/llms.txt`](/llms.txt) (every page with a one-line summary) and [`/llms-full.txt`](/llms-full.txt) (the text of every page in one file), and lists the machine-readable documents on this page. The application's own screens are never served as prose to a model; the documentation is the text. ## Using it from an agent An agent that is handed the application's address and nothing else can resolve the whole surface in two requests: the catalog, then the OpenAPI document it points at. From there every operation, its abilities, its errors and the webhook events are described, and the [quickstart](/api/v1/quickstart) explains how to obtain a token. An agent that speaks MCP instead reads the server card or follows the catalog's second entry to the [MCP guide](/mcp/v1) and connects with OAuth or a personal access token. An agent that works from skill files loads the three skills and has the rules of each surface in a page apiece; one that reads briefings loads AGENTS.md and has all of it on one page. ## See also - [OpenAPI Specification](/api/v1/openapi), the document the catalog's `service-desc` link points at. - [MCP authentication](/mcp/v1/authentication), the OAuth flow the metadata documents describe. - [MCP tools reference](/mcp/v1/tools-reference), the catalogue the server card links. --- # Authentication Source: https://alm-docs.labs.zuselab.dev/api/v1/authentication Zuse Bifrost authenticates every REST API request through a **personal access token**. Session cookies, OAuth and SAML are not accepted on the API host; tokens only. The MCP server accepts the same tokens and, in addition, the OAuth 2.1 access tokens it issues when a client signs you in; see [MCP authentication](/mcp/v1/authentication). ## Token format Tokens are opaque strings of the form: ``` zg___ ``` Three fixed segments, underscore-separated: - **`zg_`**, the Zuse Bifrost prefix, so a leaked token is recognisable in a repository scan. - **``**, `live` on production or `test` on every other environment. A `test` token is refused on production with `AUTHENTICATION_REQUIRED`, so a leak across environments never grants access. - **``**, the integer primary key of the stored row, which lets the server look the token up without scanning every row's hash. - **``**, 64 hex characters of randomness. Only this segment is secret; the other three are metadata. Other properties: - A token is revealed exactly once, at the moment of creation. The server stores only a hash of the secret. - A token does not expire by default; revoke unused tokens from the settings page. - Tokens are minted from **Account Settings → API Tokens**, or over the API with `POST /v1/tokens` by a token that holds `token:create`. A minted token is attenuated: its abilities must be a subset of the caller's, and `token:create` itself is never inherited, so a leaked automation token cannot spin up a stronger one. ## Ability-scoped authorization Every token carries an explicit list of **ability strings** (see the [ability catalogue](/api/v1/abilities)). Each REST endpoint and each MCP tool checks for a specific ability before executing; a token missing the ability receives `TOKEN_MISSING_ABILITY`, naming the ability in `error.context.required_ability`. Example minimal grant for a pipeline that reports deployments and reads the project it deploys: ``` team:view project:view deployment:record scope:team: scope:project: ``` The `scope:team:` entry is **frozen** at token-mint time. Switching teams in the app never widens an existing token; issuing the token asserts intent. ## Tenant resolution When a request arrives on the API host: 1. The API validates the bearer token and attaches it to the request. 2. The server reads `scope:team:` from the token's ability list and scopes every query to that team for the duration of the request (no database write). 3. Any `scope:project:` entries restrict the token further; a request touching a project outside the allow-list returns `TENANT_MISMATCH` (HTTP 404) so existence is never leaked. 4. A guest's token is confined to the projects they were invited to, whatever abilities it carries: the token can never outrank the person. A token minted without a `scope:team:` entry fails every request with `TENANT_MISMATCH`. The settings page appends the scope automatically; only hand-crafted tokens miss it. ## Test the token The canonical probe is `GET /v1/teams/current`: ```bash curl "https://api.example.test/v1/teams/current" \ -H "Authorization: Bearer zg_live_..." ``` A `200` response with `data.id` matching the team you minted against confirms the token is valid, the bearer header was accepted, the team context was resolved, and the token carries `team:view`. `GET /v1/me` answers who the token belongs to, every team they hold and where their alerts go. For an unauthenticated health check: ```bash curl "https://api.example.test/v1/ping" ``` ## Revocation Revoke tokens from **Account Settings → API Tokens**, or with `DELETE /v1/tokens/{token}` from another token holding `token:delete` (a token cannot revoke itself). Revocations take effect on the next call across every surface. Clients still holding the token receive `401 AUTHENTICATION_REQUIRED` on their next request, and the owner is told by a Security alert when a token is minted. ## Rotation To rotate a token: 1. Mint a replacement with identical abilities. 2. Swap the secret in your integration (environment variable, secret manager, pipeline variable). 3. Revoke the old token. Webhook endpoint signing secrets follow a separate rotation flow: `POST /v1/webhook-endpoints/{endpoint}/rotate-secret`. ## Security expectations - Never commit a token to a repository. The `zg_` prefix makes a leak easy to spot. - Store tokens in a secret manager or the hosting platform's variables. Do not hard-code them in client-side JavaScript. - Grant the **minimum** ability set required. The [ability catalogue](/api/v1/abilities) lets you tailor grants precisely. ## Related - [Ability catalogue](/api/v1/abilities): every accepted ability string. - [Tenancy and scopes](/api/v1/tenancy-and-scopes): how `scope:team` and `scope:project` combine. - [Errors](/api/v1/errors#authentication-required): every auth-related error code with remediation. --- # Error Codes Source: https://alm-docs.labs.zuselab.dev/api/v1/errors Every error returned by the REST API and the MCP server uses a uniform envelope: ```json { "error": { "code": "TENANT_MISMATCH", "message": "The requested resource does not belong to the team this API token is scoped to.", "docs_url": "https://docs.example.test/api/v1/errors#tenant-mismatch", "remediation": "Use a token minted for the correct team, or switch the account's current team before minting.", "request_id": "00-abc...", "context": { "project_id": "7d1f..." } } } ``` Fields: - `code`: a stable string identifier. **Pattern-match on this.** - `message`: a human-readable summary, free to change between releases. - `docs_url`: an anchor into this page. - `remediation`: a one-sentence hint for self-correction. - `request_id`: a trace identifier. Share it with your administrator when reporting a problem. - `fields`: only present on `VALIDATION_FAILED`. Maps each offending key to a list of rule-failure messages. - `context`: only present when the thrower supplied extra context, such as `required_ability` on `TOKEN_MISSING_ABILITY` or `project_id` on `TENANT_MISMATCH`. Agents should pattern-match on `error.code`, not `error.message`. The message is free to change; the code is contractual. ## Validation ### VALIDATION_FAILED - **HTTP status:** `422` - **When:** The request body failed a validation rule, or an action refused the request on a rule of the domain: a role code already taken, a team you cannot rename, a token that would outrank its minter. - **Remediation:** Inspect `error.fields` when present; otherwise the message says which rule refused. ## Authentication and authorization ### AUTHENTICATION_REQUIRED - **HTTP status:** `401` - **When:** No `Authorization: Bearer zg_live_...` header is present, the token has been revoked, or it was minted for another environment. - **Remediation:** Mint a fresh token under **Account Settings → API Tokens** and retry. ### TOKEN_MISSING_ABILITY - **HTTP status:** `403` - **When:** The token is valid but does not carry the ability required for this endpoint or tool. - **Context:** `required_ability`, the exact ability string to grant. - **Remediation:** Mint a replacement token that includes the listed ability, or ask the team owner to grant it to your role. ### FORBIDDEN - **HTTP status:** `403` - **When:** The token carries the right ability and can see the resource, but its holder may not change it. Today this applies to webhook endpoints: only the member who registered an endpoint, or the team owner, may rotate, test, pause or delete it. - **Remediation:** Ask the endpoint's creator or the team owner to make the change, or mint the token as that member. ### TENANT_MISMATCH - **HTTP status:** `404` - **When:** The token's `scope:team:` does not match the team that owns the resource, the token is missing `scope:team:` entirely, or `scope:project:` excludes the project. - **Remediation:** Use a token minted for the correct team or project. The response is `404` rather than `403` so existence outside the scope cannot be inferred. ### RESOURCE_NOT_FOUND - **HTTP status:** `404` - **When:** The id in the path does not exist, is malformed, or is hidden by scope; or a body named a team, a target or a user that is not yours. - **Remediation:** Verify the id. The resource may have been deleted, never existed, or sit outside the token's scope. ## Lifecycle refusals These codes belong to the planning, build, release and maintenance phases and are published here so a client can prepare for them; the endpoints that raise them land with their phases. ### APPROVAL_REQUIRED - **HTTP status:** `422` - **When:** The change needs an approval that has not been given yet. - **Remediation:** Submit the artefact for review and wait for the decision, or ask an approver named in `error.context` to decide it. ### BASELINE_LOCKED - **HTTP status:** `422` - **When:** The artefact is frozen in a baseline and can only change through an approved change request. - **Remediation:** Open a change request against the baseline named in `error.context`; once it is approved the artefact can be edited again. ### TRANSITION_NOT_ALLOWED - **HTTP status:** `422` - **When:** The item cannot move from its current status to the requested one. - **Remediation:** Read `error.context.allowed` for the statuses the item may move to. ### DEFINITION_INCOMPLETE - **HTTP status:** `422` - **When:** The item's definition checklist has unticked required entries. - **Remediation:** Tick the entries listed in `error.context.missing`, then retry the transition. ### SPRINT_ALREADY_ACTIVE - **HTTP status:** `422` - **When:** The project already has an active sprint. - **Remediation:** Close the active sprint, or move the items into it instead. ### ENVIRONMENT_PROTECTED - **HTTP status:** `422` - **When:** Deployments into this environment need a promotion approval first. - **Remediation:** Request a promotion; the deployment runs once a release manager approves it. ### AI_BUDGET_EXHAUSTED - **HTTP status:** `422` - **When:** The team's AI budget for this month is spent. - **Remediation:** Ask a team admin to raise the month's budget, or wait for the next month. ### INTEGRATION_NOT_CONNECTED - **HTTP status:** `422` - **When:** The integration this call relies on is not connected for the team or project. - **Remediation:** Connect the integration from the settings, then retry. ### LIMIT_REACHED - **HTTP status:** `422` - **When:** A limit on the project or team refused the request. - **Remediation:** Read the message for the limit that refused the request and reduce the request, or ask a team admin to raise it. ## Idempotency ### IDEMPOTENCY_KEY_MISSING - **HTTP status:** `400` - **When:** A mutating endpoint (`POST`, `PATCH`, `PUT`, `DELETE`) was called without an `Idempotency-Key` header. - **Remediation:** Generate a fresh UUID, send it as `Idempotency-Key`, retry. ### IDEMPOTENCY_KEY_REUSED - **HTTP status:** `409` - **When:** The same `Idempotency-Key` was used previously with a different request body. - **Remediation:** Use a fresh key per distinct operation. ### IDEMPOTENCY_REPLAY_IN_PROGRESS - **HTTP status:** `425` - **When:** A prior request with the same key is still running; the server is serialising concurrent retries. - **Remediation:** Wait briefly and retry. ## Request shape ### METHOD_NOT_ALLOWED - **HTTP status:** `405` - **When:** The path exists but does not accept the verb you used. - **Remediation:** Check the endpoint reference for the verbs the path accepts; the response carries an `Allow` header listing them. ## Throttling ### RATE_LIMITED - **HTTP status:** `429` - **When:** The token (or IP for unauthenticated routes) exceeded its bucket. See [rate limiting](/api/v1/rate-limiting). - **Remediation:** Back off per the `Retry-After` header. ## Webhooks ### WEBHOOK_ENDPOINT_UNREACHABLE - **HTTP status:** `502` - **When:** A synchronous call to a webhook endpoint could not reach the target. - **Remediation:** Verify the endpoint is HTTPS-reachable from the public internet. Queued deliveries are retried on the backoff ladder before dead-lettering. ## Server ### INTERNAL_SERVER_ERROR - **HTTP status:** `500` - **When:** An unexpected server-side failure. - **Remediation:** Capture `error.request_id` and report it. Every 5xx is recorded on the server side. --- # Idempotency Source: https://alm-docs.labs.zuselab.dev/api/v1/idempotency Every `POST`, `PATCH`, `PUT` and `DELETE` request against `/v1/*` requires an `Idempotency-Key` header. The key makes retries safe: replaying with the same key returns the cached response, while replaying the same key with a different body is rejected. ## Header contract ```http POST /v1/webhook-endpoints HTTP/1.1 Authorization: Bearer zg_live_... Idempotency-Key: 0199a4ae-6c02-7b5c-9f51-c5f0da5e2f12 Content-Type: application/json ``` - An opaque string, **required** on every mutation. - Any format works; generate a UUID per call. - Omit the header and the API returns `IDEMPOTENCY_KEY_MISSING` (HTTP 400). ## Replay semantics | Condition | Server response | Reply header | | ------------------------------------- | ------------------------------------ | ------------------------- | | First time this key is seen | Fresh response, normal HTTP status | | | Same key, same request body hash | Cached response, same HTTP status | `Idempotent-Replay: true` | | Same key, different request body hash | `409 IDEMPOTENCY_KEY_REUSED` | | | Same key, concurrent in-flight | `425 IDEMPOTENCY_REPLAY_IN_PROGRESS` | | Successful `2xx` JSON responses are cached under a hash of the token, method, path and key for **24 hours**, with the response body encrypted at rest, so the one-time secrets in a token mint or a webhook endpoint registration never sit in the cache in clear text. After that, the same key rotates out of the cache and behaves as a fresh request. `4xx` and `5xx` responses are **not** cached; callers are free to retry them. ## Request body fingerprint Reuse detection hashes the full raw request body. Two requests are "the same" when their token, method, path, key and body all match byte for byte, which keeps accidental reuse obvious. ## Client-side recipe A safe retry loop: ```php $key = (string) Str::uuid(); $response = Http::withHeaders([ 'Authorization' => "Bearer {$token}", 'Idempotency-Key' => $key, 'Content-Type' => 'application/json', ])->retry( times: 3, sleepMilliseconds: 500, when: fn ($exception, $request) => $exception?->response?->status() >= 500, )->post($apiUrl.'/v1/webhook-endpoints', [ 'name' => 'Deploy hook', 'url' => 'https://hooks.example.test/zusegenius', 'events' => ['project.created'], ]); ``` If the first attempt succeeds and the retry fires anyway (your client timed out on a `200` it never saw), the second call hits the cache and returns the same response; you never double-register the endpoint. Cached replays carry `Idempotent-Replay: true` so your integration can recognise them and skip any client-side side effects that might otherwise fire twice. ## What does not require a key - `GET` requests, which are naturally idempotent. - `/v1/ping`. - Webhook deliveries from Zuse Bifrost to your endpoint, which carry their own event id for consumer-side de-duplication. ## Related - [Rate limiting](/api/v1/rate-limiting) - [Error envelope](/api/v1/errors#idempotency-key-missing) --- # REST API Source: https://alm-docs.labs.zuselab.dev/api/v1 The REST API gives pipelines, scripts and other systems the same record the app shows, under the same permissions. A personal access token carries the abilities it was minted with and the team or project it is scoped to; every endpoint declares the ability it needs; every write accepts an idempotency key and every change emits a webhook event. Every response wraps its payload in `data`, every list in `data` with `meta` and `links`, and every failure in one `error` envelope with a stable code. Ids are strings, timestamps ISO 8601. ## What is live today | Resource | Routes | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Ping | `GET /v1/ping` | | Me | `GET /v1/me` | | Notifications | `GET /v1/me/notifications`, `POST /v1/me/notifications/{notification}/read`, `POST /v1/me/notifications/read-all` | | Alert destinations | `GET /v1/me/alert-destinations`, `PUT /v1/me/alert-destinations` | | Teams | `GET /v1/teams`, `GET /v1/teams/current`, `GET /v1/teams/{team}`, `POST /v1/teams`, `PATCH /v1/teams/{team}`, `DELETE /v1/teams/{team}` | | Team members and invitations | `GET /v1/teams/{team}/members`, `GET …/members/{member}`, `POST …/members`, `PATCH …/members/{member}/role`, `DELETE …/members/{member}`, `GET …/invitations`, `DELETE …/invitations/{invitation}` | | Roles | `GET /v1/roles`, `GET /v1/roles/{role}`, `POST /v1/roles`, `PATCH /v1/roles/{role}`, `DELETE /v1/roles/{role}` | | Groups | `GET /v1/groups`, `GET /v1/groups/{group}`, `POST /v1/groups`, `PATCH /v1/groups/{group}`, `PUT /v1/groups/{group}/members`, `DELETE /v1/groups/{group}` | | Tokens | `GET /v1/tokens`, `GET /v1/tokens/{token}`, `POST /v1/tokens`, `DELETE /v1/tokens/{token}` | | Webhook endpoints | `GET /v1/webhook-endpoints`, `GET …/{endpoint}`, `POST /v1/webhook-endpoints`, `POST …/{endpoint}/pause`, `…/resume`, `…/rotate-secret`, `…/test`, `DELETE …/{endpoint}` | | Webhook deliveries and events | `GET /v1/webhook-deliveries`, `GET …/{delivery}`, `POST …/{delivery}/retry`, `POST /v1/webhook-deliveries/retry-dead`, `GET /v1/webhook-events` | | Activity | `GET /v1/activity?subject_type=…&subject_id=…` | | Projects | `GET /v1/projects`, `GET /v1/projects/{project}`, `POST /v1/projects`, `PATCH /v1/projects/{project}`, `POST /v1/projects/{project}/archive`, `POST /v1/projects/{project}/restore`, `DELETE /v1/projects/{project}` | | Clients | `GET /v1/clients`, `GET /v1/clients/{client}`, `POST /v1/clients`, `PATCH /v1/clients/{client}`, `POST /v1/clients/{client}/archive`, `POST /v1/clients/{client}/restore`, `DELETE /v1/clients/{client}` | | Components | `GET /v1/projects/{project}/components`, `POST /v1/projects/{project}/components`, `GET /v1/components/{component}`, `PATCH …/{component}`, `POST …/{component}/archive`, `POST …/{component}/restore`, `DELETE …/{component}` | | Environments | `GET /v1/projects/{project}/environments`, `POST /v1/projects/{project}/environments`, `GET /v1/environments/{environment}`, `PATCH …/{environment}`, `POST …/{environment}/freeze`, `…/unfreeze`, `…/health`, `DELETE …/{environment}` | | Stakeholders | `GET /v1/projects/{project}/stakeholders`, `POST /v1/projects/{project}/stakeholders`, `GET /v1/stakeholders/{stakeholder}`, `PATCH …/{stakeholder}`, `DELETE …/{stakeholder}` | | Personas | `GET /v1/projects/{project}/personas`, `POST /v1/projects/{project}/personas`, `GET /v1/personas/{persona}`, `PATCH …/{persona}`, `POST …/{persona}/archive`, `POST …/{persona}/restore`, `DELETE …/{persona}` | | Epics | `GET /v1/projects/{project}/epics`, `POST /v1/projects/{project}/epics`, `GET /v1/epics/{epic}`, `PATCH …/{epic}`, `POST …/{epic}/status`, `DELETE …/{epic}`, `GET …/{epic}/revisions`, `GET …/revisions/{revision}`, `POST …/revisions/{revision}/restore` | | Work items | `GET /v1/projects/{project}/work-items`, `POST /v1/projects/{project}/work-items`, `GET /v1/me/work-items`, `GET /v1/work-items/{item}`, `PATCH …/{item}`, `POST …/{item}/status`, `POST …/{item}/assign`, `PUT …/{item}/acceptance-criteria`, `DELETE …/{item}`, `GET …/{item}/revisions`, `GET …/revisions/{revision}`, `POST …/revisions/{revision}/restore` | | Requirements | `GET /v1/projects/{project}/requirements`, `POST /v1/projects/{project}/requirements`, `GET /v1/requirements/{requirement}`, `PATCH …/{requirement}`, `POST …/{requirement}/status`, `DELETE …/{requirement}`, `GET …/{requirement}/revisions`, `GET …/revisions/{revision}`, `POST …/revisions/{revision}/restore` | | Business rules | `GET /v1/projects/{project}/business-rules`, `POST /v1/projects/{project}/business-rules`, `GET /v1/business-rules/{rule}`, `PATCH …/{rule}`, `POST …/{rule}/status`, `DELETE …/{rule}`, `GET …/{rule}/revisions`, `GET …/revisions/{revision}`, `POST …/revisions/{revision}/restore` | | Use cases | `GET /v1/projects/{project}/use-cases`, `POST /v1/projects/{project}/use-cases`, `GET /v1/use-cases/{useCase}`, `PATCH …/{useCase}`, `POST …/{useCase}/status`, `PUT …/{useCase}/steps`, `DELETE …/{useCase}`, `GET …/{useCase}/revisions`, `GET …/revisions/{revision}`, `POST …/revisions/{revision}/restore` | | RAID log | `GET /v1/projects/{project}/raid-items`, `POST /v1/projects/{project}/raid-items`, `GET /v1/raid-items/{raidItem}`, `PATCH …/{raidItem}`, `POST …/{raidItem}/status`, `DELETE …/{raidItem}`, `GET …/{raidItem}/revisions`, `GET …/revisions/{revision}`, `POST …/revisions/{revision}/restore` | | Decisions | `GET /v1/projects/{project}/decisions`, `POST /v1/projects/{project}/decisions`, `GET /v1/decisions/{decision}`, `PATCH …/{decision}`, `POST …/{decision}/status`, `DELETE …/{decision}`, `GET …/{decision}/revisions`, `GET …/revisions/{revision}`, `POST …/revisions/{revision}/restore` | | Labels | `GET /v1/projects/{project}/labels`, `POST /v1/projects/{project}/labels`, `PATCH /v1/labels/{label}`, `DELETE /v1/labels/{label}`; pinned through `label_ids` on the work item endpoints | | Links | `GET /v1/projects/{project}/links?artefact_type=…&artefact_id=…`, `POST /v1/projects/{project}/links`, `DELETE /v1/links/{link}` | | Attachments | `GET /v1/projects/{project}/attachments?artefact_type=…&artefact_id=…`, `POST /v1/projects/{project}/attachments` (multipart), `GET /v1/attachments/{attachment}/download`, `DELETE /v1/attachments/{attachment}` | | Comments | `GET /v1/projects/{project}/comments?artefact_type=…&artefact_id=…`, `POST /v1/projects/{project}/comments`, `PATCH /v1/comments/{comment}`, `POST …/{comment}/resolve`, `POST …/{comment}/reopen`, `DELETE …/{comment}` | | Watchers | `GET /v1/projects/{project}/watchers?artefact_type=…&artefact_id=…`, `POST /v1/projects/{project}/watchers`, `DELETE /v1/watchers/{watcher}` | | Glossary | `GET /v1/projects/{project}/glossary`, `POST /v1/projects/{project}/glossary`, `GET /v1/glossary/{term}`, `PATCH …/{term}`, `POST …/{term}/status`, `DELETE …/{term}` | | Data entities | `GET /v1/projects/{project}/data-entities`, `POST /v1/projects/{project}/data-entities`, `GET /v1/data-entities/{entity}`, `PATCH …/{entity}`, `POST …/{entity}/status`, `PUT …/{entity}/fields`, `DELETE …/{entity}` | | Design references | `GET /v1/projects/{project}/design-references`, `POST /v1/projects/{project}/design-references`, `GET /v1/design-references/{reference}`, `PATCH …/{reference}`, `DELETE …/{reference}` | | Repositories | `GET /v1/projects/{project}/repositories`, `POST /v1/projects/{project}/repositories`, `GET /v1/repositories/{repository}`, `PATCH …/{repository}`, `POST …/{repository}/disconnect`, `POST …/{repository}/reconnect`, `DELETE …/{repository}` | | Project settings | `GET /v1/projects/{project}/settings`, `PATCH /v1/projects/{project}/settings` | The [API Reference](/api/v1/reference) has one page per operation, generated from the API's own [OpenAPI document](/api/v1/openapi): the fields, the refusals, the events it fires, the MCP tools that wrap it, and a playground that calls the live API. The families above are the ones [the ability catalogue](/api/v1/abilities#enforced-today) marks as live. ## Start here --- # OpenAPI Specification Source: https://alm-docs.labs.zuselab.dev/api/v1/openapi Zuse Bifrost publishes a full OpenAPI 3.1 description of every `/v1/*` endpoint and every outbound webhook event. The document is generated from the application itself: the request validation rules, the API resources, the return types and the webhook catalogue are all inspected, and its prose is maintained beside that code, so there is no hand-written YAML to rot. The same document builds this site's [API Reference](/api/v1/reference), one page per operation with its playground, and the webhooks' [Event Reference](/webhooks/v1/events), one page per event. What you read there is exactly what a client generated from the spec sees. ## Where it lives Both files are served from the API host, the address your tokens call, and regenerated on every release so neither drifts from the code. | Path | Purpose | | ----------------- | -------------------------------------------------------------------------------------- | | `/openapi.json` | The OpenAPI document. | | `/abilities.json` | The full ability catalogue as JSON, convenient for a token-minting screen of your own. | ```bash curl https://api.example.test/openapi.json -o zusegenius-openapi.json ``` Replace `api.example.test` with your workspace's API host. ## What the document carries - **`paths`**: every operation with a summary, a description covering tenancy, behaviour and refusals, every parameter and body field described with its type and constraints, the error envelope on each failure status, and request and response examples. - **`webhooks`**: every outbound event, keyed by its name, described as the `POST` your endpoint receives: the signed envelope, the `data` object field by field, a named payload example, and the `ZG-*` headers. - **`components.securitySchemes.bearerToken`**: the personal access token, so a generated client or a playground knows to send `Authorization: Bearer`. - **`servers`**: the one host the document describes. ## Vendor extensions Every operation carries three `x-` extensions, and every webhook entry carries the first of them: | Extension | Value | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `x-required-scopes` | The ability strings the calling token must carry; on a webhook, the ability a token needs to subscribe an endpoint to the event. | | `x-webhook-events` | The webhook event names the operation can fire. | | `x-mcp-tools` | The MCP tools that wrap the same operation. | ```json { "paths": { "/v1/teams/{team}/members": { "post": { "summary": "Invite a team member", "x-required-scopes": ["team-member:invite"], "x-webhook-events": ["team.member_invited"], "requestBody": { "...": "..." } } } } } ``` Tooling reads them to generate permission-aware clients and agent toolkits without inspecting the server, and the reference pages on this site render them as the badge row above each operation. ## Typical consumers - **Postman and Insomnia**: import the JSON URL and let the app build a collection by tag; every write needs an `Idempotency-Key` header, which the reference explains. - **openapi-generator, Stainless, Speakeasy, Fern**: produce a typed client in any language. `x-required-scopes` ends up as comments in the generated source. - **LangChain and custom agents**: expose every endpoint as a tool, or hand the spec to an agent beside a personal access token and let it compose requests. The [MCP server](/mcp/v1) is the purpose-built door for that. - **Webhook consumers**: generate the payload types for your handlers from the `webhooks` object, so a renamed field is a compile error rather than a silent `undefined`. ## Related - [API Reference](/api/v1/reference): the document rendered as pages, with a playground on each. - [Event Reference](/webhooks/v1/events): every webhook event's envelope and payload, from the same document. - [Ability catalogue](/api/v1/abilities): the strings that appear in `x-required-scopes`. - [Errors](/api/v1/errors): the envelope every operation can return on failure. --- # Pagination Source: https://alm-docs.labs.zuselab.dev/api/v1/pagination Every paginated list endpoint returns a `data` array wrapped in a `meta` + `links` envelope. Pagination is page-based, capped at 100 rows per request. ## Response shape ```json { "data": [ { "id": "7d1f...", "key": "CRM", "name": "CRM Replacement" }, { "id": "9a3c...", "key": "PAY", "name": "Payments" } ], "links": { "first": "https://api.example.test/v1/projects?page=1", "last": "https://api.example.test/v1/projects?page=4", "prev": null, "next": "https://api.example.test/v1/projects?page=2" }, "meta": { "current_page": 1, "from": 1, "last_page": 4, "per_page": 25, "to": 25, "total": 92 } } ``` ## The `per_page` and `page` parameters Every paginated endpoint accepts: - **`per_page`**, the number of rows in the response (default `25`, maximum `100`). `limit` is accepted as an alias. - **`page`**, the 1-based page index (default `1`). - **`sort_by`** and **`sort_direction`** (`asc` or `desc`), where the endpoint documents sortable columns; an unknown column falls back to the default order. ```bash curl "https://api.example.test/v1/projects?per_page=50&page=2" \ -H "Authorization: Bearer zg_live_..." ``` | Endpoint | Default `per_page` | Order | | ---------------------------- | ------------------ | ------------------------------------------------------------------- | | `GET /v1/projects` | `25` | Newest first; `sort_by` takes `name`, `key`, `status`, `created_at` | | `GET /v1/tokens` | `25` | Newest first | | `GET /v1/webhook-endpoints` | `15` | Newest first | | `GET /v1/webhook-deliveries` | `15` | Newest first | | `GET /v1/me/notifications` | `15` | Newest first | | `GET /v1/webhook-events` | `3` (max `10`) | Newest first, no envelope | Short lists are not paginated: `GET /v1/teams`, `GET /v1/teams/{team}/members`, `GET /v1/roles`, `GET /v1/groups`, `GET /v1/me/alert-destinations` and `GET /v1/activity` return every row in `data` with no `meta`. Requesting `per_page` above the cap (`100`) clamps to the maximum silently. Requesting a `page` past the last page returns an empty `data` array with `meta.total` still populated, so a polling loop can detect the end without guessing. ## Iterating across pages ```ts async function fetchAllProjects(apiUrl: string, token: string) { const all = []; let page = 1; while (true) { const res = await fetch(`${apiUrl}/v1/projects?per_page=100&page=${page}`, { headers: { Authorization: `Bearer ${token}` }, }); const body = await res.json(); all.push(...body.data); if (body.meta.current_page >= body.meta.last_page) break; page += 1; } return all; } ``` ## Filtering before paginating Combine `per_page` with the resource-specific filters to avoid long loops: ```bash curl "https://api.example.test/v1/projects?status=in_development&per_page=100" \ -H "Authorization: Bearer zg_live_..." ``` ## Related - [Rate limiting](/api/v1/rate-limiting): how hard you can poll. - [Webhooks](/webhooks/v1): the streaming alternative to polling. --- # REST API Quickstart Source: https://alm-docs.labs.zuselab.dev/api/v1/quickstart By the end of this page you will have a working `zg_` API token, a successful `GET /v1/teams/current` call, and a webhook endpoint receiving `project.created` events. ## Prerequisites - A Zuse Bifrost account in a team, with a role that carries the abilities below - A terminal with `curl`, or an HTTP client of your choice ### Mint an API token [step] Open **Account Settings → API Tokens** and press **Create Token**. Tick these abilities: - `team:view`, so the token can identify the team it is scoped to - `project:view-any` and `project:view`, read-only project access - `webhook-endpoint:create`, so the same token can register a webhook endpoint Leave the team on the one you are working in; the token receives its `scope:team:` entry from that choice. Press **Create**. The plaintext token (`zg_live_...`) is shown once; copy it now, it is not retrievable later. [API tokens](/workspace/api-tokens) covers the page in full. ### Confirm the token works [step] #### curl ```bash curl "https://api.example.test/v1/teams/current" \ -H "Authorization: Bearer zg_live_..." ``` #### PHP (Laravel Http) ```php use Illuminate\Support\Facades\Http; $response = Http::withToken('zg_live_...') ->get(config('services.zusegenius.api').'/v1/teams/current'); $team = $response->json('data'); ``` #### Node (fetch) ```ts const response = await fetch(`https://api.example.test/v1/teams/current`, { headers: { Authorization: "Bearer zg_live_..." }, }); const { data: team } = await response.json(); ``` #### Python (requests) ```python import requests response = requests.get( "https://api.example.test/v1/teams/current", headers={"Authorization": "Bearer zg_live_..."}, ) team = response.json()["data"] ``` Expected response: ```json { "data": { "id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "name": "Zuse Technologies", "owner_user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "created_at": "2026-10-26T07:12:33+00:00" } } ``` Replace `api.example.test` with your workspace's API host, which your administrator gives you with the workspace address. ### Register a webhook endpoint [step] Point a `webhook.site` URL (or any HTTPS endpoint you control) at `project.created`: ```bash curl "https://api.example.test/v1/webhook-endpoints" \ -X POST \ -H "Authorization: Bearer zg_live_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "First webhook", "url": "https://webhook.site/", "events": ["project.created"] }' ``` Expected response (`201 Created`): ```json { "data": { "id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "name": "First webhook", "events": ["project.created"], "is_active": true }, "secret": "whsec_abc123..." } ``` **Copy the `secret` now.** Later `GET /v1/webhook-endpoints` calls never include it. Use it to verify every incoming `ZG-Signature: t=...,v1=...` header; see [signature verification](/webhooks/v1/signature-verification). ### Trigger an event [step] Create a project in Zuse Bifrost. Within a few seconds the `webhook.site` URL receives a `POST` with a `project.created` envelope. ## Next steps - [Authentication](/api/v1/authentication): ability combinations, revocation, rotation - [Idempotency](/api/v1/idempotency): make your retries safe - [Webhooks quickstart](/webhooks/v1/quickstart): signature verification, retry semantics - [Errors](/api/v1/errors): every error code and its remediation --- # Rate Limiting Source: https://alm-docs.labs.zuselab.dev/api/v1/rate-limiting Every authenticated API request sits behind at least one named limiter. Buckets apply **per token**, not per IP, because that is the unit of identity pipelines and agents share. Hit a limit and you get a `429` response wrapped in the standard [error envelope](/api/v1/errors) with `code: "RATE_LIMITED"` plus a `Retry-After` header. ## Named buckets | Bucket | Applied to | Limit | | -------------- | -------------------------------------------- | --------------------------------------- | | `api-token` | Every authenticated `/v1/*` route | 300 requests/min + 10,000 requests/hour | | `webhook-test` | `POST /v1/webhook-endpoints/{endpoint}/test` | 10 test fires/min per token | When the caller is unauthenticated, the `api-token` limiter falls back to a per-IP key so accidental traffic bursts are still caught. ## Response headers Every rate-limited response carries the standard rate-limit headers: ```http X-RateLimit-Limit: 300 X-RateLimit-Remaining: 278 Retry-After: 47 ``` Honour `Retry-After` before retrying; it reports the number of seconds until the bucket refills. ## Error shape ```json { "error": { "code": "RATE_LIMITED", "message": "The rate limit for this token has been exceeded.", "remediation": "Back off and retry after the time hinted by the Retry-After header.", "docs_url": "https://docs.example.test/api/v1/errors#rate-limited", "request_id": "00-abc..." } } ``` ## Designing pipelines and agents - **Polling**: keep the interval at 15 seconds or more per poller, and prefer [webhooks](/webhooks/v1) for anything that must react quickly. - **Bulk imports**: chunk writes into batches of 100 to 200 and respect `Retry-After` when a `429` fires. Concurrency does not beat the limiter; it counts concurrent calls the same as sequential ones. - **Agents**: assume the bucket is shared across every call the agent makes with one token. Hitting the limit in practice usually means a tight loop on the agent's side. ## Upstream caps The hourly cap (10,000 requests) deliberately allows a sustained rate of about 160 requests a minute before the per-minute ceiling kicks in. If an integration needs a higher sustained rate, raise it with your administrator, who can adjust the limit for your workspace. --- # Activity API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/activity Every mutation Zuse Bifrost performs, whether from the app, the REST API, an MCP tool call, an accepted Heimdall draft or a scheduled system task, is recorded against its subject in the activity log. `GET /v1/activity` returns that log filtered to a single subject so agents and audit tools can reconstruct exactly what changed and who did it. The team-wide log, filtered by who acted, is the **Settings → Activity** page. ## Endpoints - `GET /v1/activity` — [Read a subject's activity](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity/read-a-subjects-activity) --- # Read a subject's activity Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/activity/read-a-subjects-activity `GET /v1/activity` The newest entries recorded against one subject, `limit` at a time (50 by default, 200 at most), newest first. Name the subject by alias and id: ```bash curl "https://api.example.test/v1/activity?subject_type=project&subject_id=$PROJECT_ID&limit=20" \ -H "Authorization: Bearer $TOKEN" ``` There is no paging beyond `limit`: the endpoint is a recent-history read, not an export. The subject is resolved through its own tenant-scoped query before activity is read, so passing the id of a project, client or project member that belongs to another team returns `404 RESOURCE_NOT_FOUND` rather than an empty list; existence never leaks across tenants. A project outside the token's `scope:project:` allow-list is a `404` for the same reason. - `subject_type` and `causer_type` use short morph aliases (`project`, `client`, `project-member`, `user`) rather than internal class names. Treat them as opaque strings. The `subject_type` **query parameter** accepts `project`, `client` and `project-member` today, and every artefact alias as its module lands; anything else is `422 VALIDATION_FAILED`. - `actor_kind` distinguishes the surface the change came through: `human` (the app), `api_token` (REST), `mcp` (an MCP tool), `integration` (an inbound delivery such as a Telegram command), `webhook`, `ai` (an accepted Heimdall draft) or `system` (a scheduled job). Filter on it client-side when you need "everything the agent did yesterday" separately from what people did. - `changes` holds, for an update, `attributes` (the new values) and `old` (the previous ones) of the columns that changed. - `properties` keys matching `token`, `secret`, `password`, `api_key` or `api_secret` (case-insensitive) are stripped at any depth before the response leaves the server, so a context window never sees credentials that were logged alongside a write. - Requires ability: `activity:read` - MCP tools: `get_activity_log` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `subject_type` | query | `project`, `client`, `project-member`, `component`, `environment`, `stakeholder`, `persona`, `glossary-term`, `data-entity`, `data-field`, `design-reference`, `repository`, `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision` | yes | The kind of subject, as a short alias: `project`, `client` or `project-member`. A class name is refused. | | `subject_id` | query | string (uuid) | yes | The subject's id. | | `limit` | query | integer | no | How many of the newest entries to return, 1 to 200; defaults to 50. A value above 200 is refused rather than clamped. | ### Responses - **200**: Array of `ActivityResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The entry's id. | | `data[].log_name` | string \| null | yes | The log the entry was written to: `project`, `client`, `project-member` or `user`. | | `data[].description` | string | yes | What happened, in a sentence that names the record, such as `Project 'Payments' was updated`. | | `data[].event` | string \| null | yes | The lifecycle event the entry records: `created`, `updated`, `deleted` or `restored`; null for a manual entry. | | `data[].subject_type` | string \| null | yes | The kind of thing that changed, as a short alias such as `project`, `client` or `project-member`. Treat it as an opaque string. | | `data[].subject_id` | string \| null | yes | The id of the thing that changed. | | `data[].causer_type` | string \| null | yes | The kind of actor, as a short alias such as `user`; null for a system change. | | `data[].causer_id` | string \| null | yes | The actor's id; null for a system change. | | `data[].actor_kind` | any | yes | The surface the change came through: `human` (the app), `api_token` (REST), `mcp` (an MCP tool), `integration` (an inbound delivery), `webhook`, `ai` (an accepted Heimdall draft) or `system` (a scheduled job). Filter on it client-side to separate what an agent did from what a person did. | | `data[].changes` | object | yes | For an update, `attributes` (the new values) and `old` (the previous ones) of the columns that changed; empty otherwise. | | `data[].properties` | object | yes | What was recorded with the change beyond the column diff: the `project_id` and `team_id` stamps and the `actor_kind`. Keys matching `token`, `secret`, `password`, `api_key` or `api_secret`, at any depth and in any case, are stripped before the response leaves the server. | | `data[].created_at` | string \| null | yes | When the change was recorded, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: when the subject does not exist in the caller's tenant, or belongs to a project outside the token's `scope:project:` allow-list. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `subject_type` or `subject_id` is missing, the alias is unknown, the id is not a UUID, or `limit` is above 200. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Alert Destinations API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations An **alert destination** is a place your alerts go: your email address or the Telegram chat linked to your account. Each destination carries the **classes** of alert it receives (`approvals`, `assignments`, `mentions`, `planning`, `sprints`, `code`, `testing`, `releases`, `deployments`, `incidents`, `support`, `security`, `digest`, `onboarding`, `budget`), or every class when `classes` is `null`. The app shows the same grid under **Account Settings → Notifications**. These routes are about the **caller's own** account, so they live under `/v1/me` and need no project or team in the path. ## Endpoints - `GET /v1/me/alert-destinations` — [List your alert destinations](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations/list-your-alert-destinations) - `PUT /v1/me/alert-destinations` — [Replace your alert destinations](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations/replace-your-alert-destinations) ## Related - [Me API](https://alm-docs.labs.zuselab.dev/api/v1/reference/me): The destinations alongside your teams and your Telegram link in one call. - [Notifications API](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications): The entries every alert also writes. - [Notifications](https://alm-docs.labs.zuselab.dev/account/notifications): The settings screen, linking Telegram and what each class covers. --- # List your alert destinations Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations/list-your-alert-destinations `GET /v1/me/alert-destinations` Your destinations, primary first. - `kind` is `email` or `telegram` (`push_device` is reserved for the mobile apps). - `target` is your email address for `email`, or the linked chat's id for `telegram`. - `classes` lists the alert classes routed here, is `null` for every class, or is `[]` for none (the destination is kept but receives nothing). - `is_primary` marks the destination of your linked Telegram chat. It is derived from the link, never set through this API. Until you write a destination, you are on the **default**: every class goes to your linked Telegram chat when there is one, and email carries what nothing else does. The list is empty while the default applies, and the grid in the app shows the default as every cell ticked on the chat. Linking Telegram writes the default down as rows, so from then on the rows lead. > **Critical classes always reach your email.** Security notices, an open Sev1 or Sev2 incident and an exhausted AI budget are sent to your email whatever the destinations say, because they arrive when the chat you would otherwise read them on may be the thing that failed. Routing them to Telegram adds a message there; it never removes the email. - Requires ability: `account:read` - MCP tools: `get_me` ### Responses - **200**: Array of `AlertDestinationResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The destination's id. | | `data[].kind` | string | yes | `email` for the account's address, `telegram` for the linked chat. `push_device` is reserved for the mobile apps. | | `data[].target` | string | yes | The email address for `email`, or the chat id for `telegram`. | | `data[].classes` | array \| null | yes | The alert classes routed here, from the class catalogue. `null` means every class; `[]` keeps the destination but sends it nothing. | | `data[].is_primary` | boolean | yes | Whether this is the destination of the linked chat, the one the default routing prefers. Derived, never set through the API. | | `data[].verified_at` | string \| null | yes | When the destination was confirmed reachable, ISO 8601; null while unverified. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Replace your alert destinations Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations/replace-your-alert-destinations `PUT /v1/me/alert-destinations` Takes the complete list you want to keep; anything not in it is removed. Each entry names a `kind`, a `target` and optional `classes`: ```bash curl -X PUT https://api.example.test/v1/me/alert-destinations \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "destinations": [ { "kind": "telegram", "target": "123456789", "classes": ["approvals", "assignments", "mentions", "incidents"] }, { "kind": "email", "target": "priya@example.com", "classes": null } ] }' ``` The response is the resulting list. `PUT` is a replacement, not a patch: send an empty `destinations` list to remove every row and return to the default. `is_primary` in the payload is ignored; the service derives it from your Telegram link. When an alert is sent, the Telegram chat receives a message when it is routed to the alert's class, and email is used when an email destination routes the class, when nothing else does, or when the class is critical. The same routing decides whether the bot messages you at all: a class you route away from the chat is never sent there. Quiet hours set in the app delay what is not critical. - Requires ability: `account:write` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The complete set of alert destinations to keep. Anything not listed is removed; an empty list returns you to the default routing. | Field | Type | Required | Description | | --- | --- | --- | --- | | `destinations` | array of object | yes | Every destination you want, at most 20. The key must be present; an empty list removes every row and restores the default. | | `destinations[].kind` | `email`, `telegram`, `push_device` | yes | `email` for your address or `telegram` for your linked chat. | | `destinations[].target` | string | yes | Your email address, or your linked chat's id. Anything that is not yours is refused with `404 RESOURCE_NOT_FOUND` and nothing is written. | | `destinations[].classes` | array \| null | no | The alert classes to route here. Send `null` for every class or `[]` for none. `is_primary` is not accepted; the service derives it. | ### Responses - **200**: Array of `AlertDestinationResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The destination's id. | | `data[].kind` | string | yes | `email` for the account's address, `telegram` for the linked chat. `push_device` is reserved for the mobile apps. | | `data[].target` | string | yes | The email address for `email`, or the chat id for `telegram`. | | `data[].classes` | array \| null | yes | The alert classes routed here, from the class catalogue. `null` means every class; `[]` keeps the destination but sends it nothing. | | `data[].is_primary` | boolean | yes | Whether this is the destination of the linked chat, the one the default routing prefers. Derived, never set through the API. | | `data[].verified_at` | string \| null | yes | When the destination was confirmed reachable, ISO 8601; null while unverified. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: when a `target` is not your own linked chat or your own address; nothing is written. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `destinations` is missing, holds more than 20 entries, or an entry has an unknown `kind` or class. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Decide an approval Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/decide-an-approval `POST /v1/approvals/{approval}/decide` ```bash curl -X POST https://api.example.test/v1/approvals/$APPROVAL_ID/decide \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"verdict": "changes_requested", "comment": "Name the voucher kinds it applies to."}' ``` Writes the token holder's verdict on a pending request that names them, once per round, with the address and the client it came from behind a signature hash. `approved` counts towards what the rule needs and carries the request when enough approvers agree, stamping the record approved at the next version; `rejected` closes the request and returns the record to where it stood; `changes_requested` holds it for the author. `approval.decided` goes out for every verdict, the outcome's own event beside it when one settles. Answers `200` with the request as it stands. - Requires ability: `approval:decide` - Fires events: `approval.decided` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `approval` | path | string (uuid) | yes | The request. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) An approver's verdict on a pending request, with the reason when the policy asks for one. | Field | Type | Required | Description | | --- | --- | --- | --- | | `verdict` | `approved`, `rejected`, `changes_requested` | yes | What the approver says: `approved`, `rejected` or `changes_requested`. A rejection closes the request and returns the record to where it stood; changes requested holds the request for the author to edit and resubmit. | | `comment` | string \| null | no | Why, up to 2000 characters. Required for `rejected` and `changes_requested` when the project's policy says so. | ### Responses - **200**: The request as it stands after the verdict. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the request does not name the token holder as an approver, or the token lacks `approval:decide`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the id is unknown or hidden from the token. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `verdict` is missing or unknown, the request is no longer waiting for a decision, the holder decided this round already, or a reason is required by the policy and missing. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get an approval request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/get-an-approval-request `GET /v1/approvals/{approval}` ```bash curl https://api.example.test/v1/approvals/$APPROVAL_ID \ -H "Authorization: Bearer $TOKEN" ``` One request with every verdict given on it, every round, oldest first, each with the approver's name and the signature hash, beside the approvers the policy named, the tally and the clocks. - Requires ability: `approval:view` - MCP tools: `get_approval_request` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `approval` | path | string (uuid) | yes | The request. | ### Responses - **200**: The request. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the id is unknown, the request belongs to a project the token may not see, or a client guest's token asks for a request that does not name them. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Approvals API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals An **approval request** is one time a planning record was put up for approval: an epic, a story, a requirement, a business rule or a use case, a baseline or a change request, submitted by its author and decided by the approvers the project's **policy** names for that kind of approval. Submitting binds the request to the version of the record as it stood, moves the record into review where it has a review status, and tells the approvers. Each approver writes one signed verdict per round: approve, reject, or ask for changes. The policy's rule says how many approvals carry the request (any one, every approver, or a quorum); one rejection closes it and returns the record to where it stood; a request for changes holds it until the author edits the record and resubmits, which opens the next round. A request that carries stamps the record approved at the next version, bound to the revision that was reviewed. Every policy sets clocks: the hours the approvers have, when the undecided ones are nudged, and when a request may carry by itself; the hourly sweep rings them, escalating an overdue request to the project's lead. A project that never changed a kind reads the defaults; the policy endpoint shows which. Deciding is a person's act: the submit door is open to agents, the decide door is not. A client guest's token reads and decides only the requests that name them (through a stakeholder seat linked to their account) and submits nothing. Every write runs the same action the pages run and announces itself as an `approval.*` event; the move an approval makes on the record itself is announced by the record's own family (`requirement.status_changed` and its siblings). ## Endpoints - `GET /v1/approvals` — [List the inbox](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-the-inbox) - `GET /v1/approvals/{approval}` — [Get an approval request](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/get-an-approval-request) - `GET /v1/projects/{project}/approval-policies` — [List a project's approval policies](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-a-projects-approval-policies) - `PUT /v1/projects/{project}/approval-policies` — [Update a project's approval policy](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/update-a-projects-approval-policy) - `GET /v1/projects/{project}/approvals` — [List a project's approvals](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-a-projects-approvals) - `POST /v1/approvals/{approval}/decide` — [Decide an approval](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/decide-an-approval) - `POST /v1/approvals/{approval}/withdraw` — [Withdraw an approval](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/withdraw-an-approval) - `POST /v1/baselines/{baseline}/submit` — [Submit a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-baseline) - `POST /v1/change-requests/{changeRequest}/submit` — [Submit a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-change-request) - `POST /v1/business-rules/{rule}/submit` — [Submit a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-business-rule) - `POST /v1/requirements/{requirement}/submit` — [Submit a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-requirement) - `POST /v1/use-cases/{useCase}/submit` — [Submit a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-use-case) - `POST /v1/epics/{epic}/submit` — [Submit an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-an-epic) - `POST /v1/work-items/{item}/submit` — [Submit a story](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-story) ## Related - [Requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements): [Epics](/api/v1/reference/epics), [Work items](/api/v1/reference/work-items), [Business rules](/api/v1/reference/business-rules), [Use cases](/api/v1/reference/use-cases): the records an approval decides, with their versions. - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders): The client seats a policy may name. - [approval.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval): The life of an approval request: an artefact put up for approval, each approver's verdict, the outcome the verdicts settle, a request taken back, a request that ran past its time, and a change to how a project decides a kind of approval. --- # List a project's approval policies Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-a-projects-approval-policies `GET /v1/projects/{project}/approval-policies` ```bash curl https://api.example.test/v1/projects/$PROJECT_ID/approval-policies \ -H "Authorization: Bearer $TOKEN" ``` One policy per kind of approval the project can configure today, each saying whether the project saved it (`stored`) or still reads the defaults. - Requires ability: `approval:view-any` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | ### Responses - **200**: Array of `ApprovalPolicyResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].kind` | string | yes | The gate: `requirement_review` today; the other kinds join with their modules. | | `data[].rule` | string | yes | How many approvals carry a request: `any`, `all` or `quorum`. | | `data[].quorum` | integer | yes | The quorum under the quorum rule; at least 1. | | `data[].approver_roles` | array of any | yes | The project roles whose holders approve: `owner`, `project_manager`, `business_analyst`, `developer`, `qa_engineer`, `release_manager` or `viewer`. | | `data[].approver_user_ids` | array of string | yes | The people named by id; each must be a member of the team. | | `data[].approver_stakeholder_ids` | array of string | yes | The stakeholders named by id; only a seat with a linked account can decide. | | `data[].sla_hours` | integer \| null | yes | Hours the approvers have before the request is overdue; null for no clock. | | `data[].reminder_hours` | integer \| null | yes | Hours after which approvers who have not answered are nudged; null for none. | | `data[].auto_approve_after_hours` | integer \| null | yes | Hours after which an unanswered request carries by itself; null for never. | | `data[].requires_comment_on_reject` | boolean | yes | Whether a rejection or a request for changes needs a reason. | | `data[].enabled` | boolean | yes | Whether the gate is on; off, a submission carries at once. | | `data[].stored` | boolean | yes | Whether the project saved this policy; false while it reads the defaults. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `approval:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List a project's approvals Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-a-projects-approvals `GET /v1/projects/{project}/approvals` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/approvals?status=pending" \ -H "Authorization: Bearer $TOKEN" ``` Every request on one project, open and settled, 25 per page, newest first, with the same filters, sorting and rows as the inbox; `scope` is ignored here. A client guest's token reads only the requests that name them. - Requires ability: `approval:view-any` - MCP tools: `list_pending_approvals` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `scope` | query | `waiting`, `raised`, `all` \| null | no | Which view of the inbox: `waiting` for what still waits for the token holder's own decision (the default), `raised` for what they submitted, `all` for everything they may see. Ignored on a project's list, which is always `all`. | | `status` | query | `pending`, `approved`, `rejected`, `changes_requested`, `withdrawn`, `expired` \| null | no | Narrow to one status: `pending`, `approved`, `rejected`, `changes_requested`, `withdrawn` or `expired`. | | `kind` | query | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` \| null | no | Narrow to one kind of approval, such as `requirement_review`. | | `artefact_type` | query | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `glossary-term`, `data-entity`, `persona`, `stakeholder`, `baseline`, `change-request` \| null | no | Narrow to one family of record under approval: `epic`, `work-item`, `requirement`, `business-rule` or `use-case`. | | `project_id` | query | string \| null (uuid) | no | Narrow the inbox to one project, by id. | | `search` | query | string \| null | no | Words the reference or the title must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of requests, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The request's id. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].approvable_type` | string | yes | The artefact's family: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `baseline` or `change-request`. | | `data[].approvable_id` | string | yes | The artefact's id. | | `data[].reference` | string | yes | The artefact's key as submitted, such as `CRM-FR-12`. | | `data[].title` | string | yes | The artefact's title as submitted. | | `data[].kind` | string | yes | The gate: `requirement_review`, `baseline_approval`, `client_sign_off` or `change_request` today; the other kinds join with their modules. | | `data[].status` | string | yes | Where it stands: `pending`, `approved`, `rejected`, `changes_requested`, `withdrawn` or `expired`. | | `data[].rule` | string | yes | How many approvals carry it: `any`, `all` or `quorum`, copied from the policy when it opened. | | `data[].quorum` | integer | yes | The quorum under the quorum rule. | | `data[].round` | integer | yes | 1 for the first submission; one more for each resubmission after changes were requested. | | `data[].approvers` | array of object | yes | The people asked, resolved from the policy when the round opened. | | `data[].approvers[].user_id` | string | yes | The approver's id. | | `data[].approvers[].name` | string | yes | Their name, or the stakeholder seat's, as it read when asked. | | `data[].approvers[].via` | string | yes | How the policy found them: `owner`, `role`, `user` or `stakeholder`. | | `data[].approvers[].stakeholder_id` | string \| null | yes | The stakeholder seat, when found through one; null otherwise. | | `data[].tally` | object | no | Where the approvals stand this round; present when the caller counted it. | | `data[].tally.approved` | integer | yes | Approvals given this round. | | `data[].tally.needed` | integer | yes | Approvals the rule needs. | | `data[].tally.total` | integer | yes | Approvers named. | | `data[].requires_comment` | boolean | yes | Whether a rejection or a request for changes needs a reason. | | `data[].requested_by` | string \| null | yes | The person who submitted it; null when the account is gone. | | `data[].requester_name` | string \| null | yes | That person's name, when the requester was loaded; null otherwise. | | `data[].summary` | string \| null | yes | What changed and why, as the author wrote it for the approvers; null when none. | | `data[].revision_id` | string \| null | yes | The version of the artefact under review; null for an artefact never captured. | | `data[].due_at` | string \| null | yes | When the approvers' time runs out, ISO 8601; null when the policy sets no clock. | | `data[].remind_at` | string \| null | yes | When approvers who have not answered are nudged, ISO 8601; null for none. | | `data[].auto_approve_at` | string \| null | yes | When the request carries by itself, ISO 8601; null for never. | | `data[].reminded_at` | string \| null | yes | When the nudge was sent this round, ISO 8601; null until then. | | `data[].escalated_at` | string \| null | yes | When the overdue request was escalated to the project's lead, ISO 8601; null until then. | | `data[].decided_at` | string \| null | yes | When the request settled, ISO 8601; null while open. | | `data[].auto_approved` | boolean | yes | Whether it carried by itself: the gate was off, or the time the policy allows passed with no decision. | | `data[].decisions` | array of object | no | Every verdict given, every round, oldest first; present when the decisions were loaded. | | `data[].decisions[].id` | string | yes | The decision's id. | | `data[].decisions[].approval_request_id` | string | yes | The request answered. | | `data[].decisions[].round` | integer | yes | The round the verdict answers. | | `data[].decisions[].approver_id` | string \| null | yes | The person who decided; null when the account is gone. | | `data[].decisions[].approver_name` | string \| null | yes | Their name, when the approver was loaded; null otherwise. | | `data[].decisions[].stakeholder_id` | string \| null | yes | The stakeholder seat a client guest decided from; null for staff. | | `data[].decisions[].verdict` | string | yes | What they said: `approved`, `rejected` or `changes_requested`. | | `data[].decisions[].comment` | string \| null | yes | The reason they gave; null when none. | | `data[].decisions[].signature_hash` | string | yes | SHA-256 over the request, the round, the person, the verdict and the moment, for the audit export. | | `data[].decisions[].decided_at` | string | yes | When the verdict was given, ISO 8601. | | `data[].created_at` | string \| null | yes | When the request was submitted, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the request last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `approval:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a filter is not one of its values. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List the inbox Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-the-inbox `GET /v1/approvals` ```bash curl "https://api.example.test/v1/approvals?scope=waiting" \ -H "Authorization: Bearer $TOKEN" ``` The approvals inbox of the token's holder across every project the token may see, 25 per page (`per_page` 1 to 100), newest first. `scope` picks the view: `waiting` (the default) is what still waits for their own decision, leaving out the rounds they already answered; `raised` is what they submitted; `all` is everything they may see, which for a client guest is only the requests that name them. `status`, `kind`, `artefact_type` and `project_id` narrow the page, and `search` to the requests whose reference or title contain the words. `sort_by` takes `reference`, `title`, `kind`, `status`, `round`, `due_at`, `decided_at`, `created_at` or `updated_at`. Each row carries its approvers and a `tally` of where the approvals stand. - Requires ability: `approval:view-any` - MCP tools: `list_pending_approvals` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `scope` | query | `waiting`, `raised`, `all` \| null | no | Which view of the inbox: `waiting` for what still waits for the token holder's own decision (the default), `raised` for what they submitted, `all` for everything they may see. Ignored on a project's list, which is always `all`. | | `status` | query | `pending`, `approved`, `rejected`, `changes_requested`, `withdrawn`, `expired` \| null | no | Narrow to one status: `pending`, `approved`, `rejected`, `changes_requested`, `withdrawn` or `expired`. | | `kind` | query | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` \| null | no | Narrow to one kind of approval, such as `requirement_review`. | | `artefact_type` | query | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `glossary-term`, `data-entity`, `persona`, `stakeholder`, `baseline`, `change-request` \| null | no | Narrow to one family of record under approval: `epic`, `work-item`, `requirement`, `business-rule` or `use-case`. | | `project_id` | query | string \| null (uuid) | no | Narrow the inbox to one project, by id. | | `search` | query | string \| null | no | Words the reference or the title must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of requests, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The request's id. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].approvable_type` | string | yes | The artefact's family: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `baseline` or `change-request`. | | `data[].approvable_id` | string | yes | The artefact's id. | | `data[].reference` | string | yes | The artefact's key as submitted, such as `CRM-FR-12`. | | `data[].title` | string | yes | The artefact's title as submitted. | | `data[].kind` | string | yes | The gate: `requirement_review`, `baseline_approval`, `client_sign_off` or `change_request` today; the other kinds join with their modules. | | `data[].status` | string | yes | Where it stands: `pending`, `approved`, `rejected`, `changes_requested`, `withdrawn` or `expired`. | | `data[].rule` | string | yes | How many approvals carry it: `any`, `all` or `quorum`, copied from the policy when it opened. | | `data[].quorum` | integer | yes | The quorum under the quorum rule. | | `data[].round` | integer | yes | 1 for the first submission; one more for each resubmission after changes were requested. | | `data[].approvers` | array of object | yes | The people asked, resolved from the policy when the round opened. | | `data[].approvers[].user_id` | string | yes | The approver's id. | | `data[].approvers[].name` | string | yes | Their name, or the stakeholder seat's, as it read when asked. | | `data[].approvers[].via` | string | yes | How the policy found them: `owner`, `role`, `user` or `stakeholder`. | | `data[].approvers[].stakeholder_id` | string \| null | yes | The stakeholder seat, when found through one; null otherwise. | | `data[].tally` | object | no | Where the approvals stand this round; present when the caller counted it. | | `data[].tally.approved` | integer | yes | Approvals given this round. | | `data[].tally.needed` | integer | yes | Approvals the rule needs. | | `data[].tally.total` | integer | yes | Approvers named. | | `data[].requires_comment` | boolean | yes | Whether a rejection or a request for changes needs a reason. | | `data[].requested_by` | string \| null | yes | The person who submitted it; null when the account is gone. | | `data[].requester_name` | string \| null | yes | That person's name, when the requester was loaded; null otherwise. | | `data[].summary` | string \| null | yes | What changed and why, as the author wrote it for the approvers; null when none. | | `data[].revision_id` | string \| null | yes | The version of the artefact under review; null for an artefact never captured. | | `data[].due_at` | string \| null | yes | When the approvers' time runs out, ISO 8601; null when the policy sets no clock. | | `data[].remind_at` | string \| null | yes | When approvers who have not answered are nudged, ISO 8601; null for none. | | `data[].auto_approve_at` | string \| null | yes | When the request carries by itself, ISO 8601; null for never. | | `data[].reminded_at` | string \| null | yes | When the nudge was sent this round, ISO 8601; null until then. | | `data[].escalated_at` | string \| null | yes | When the overdue request was escalated to the project's lead, ISO 8601; null until then. | | `data[].decided_at` | string \| null | yes | When the request settled, ISO 8601; null while open. | | `data[].auto_approved` | boolean | yes | Whether it carried by itself: the gate was off, or the time the policy allows passed with no decision. | | `data[].decisions` | array of object | no | Every verdict given, every round, oldest first; present when the decisions were loaded. | | `data[].decisions[].id` | string | yes | The decision's id. | | `data[].decisions[].approval_request_id` | string | yes | The request answered. | | `data[].decisions[].round` | integer | yes | The round the verdict answers. | | `data[].decisions[].approver_id` | string \| null | yes | The person who decided; null when the account is gone. | | `data[].decisions[].approver_name` | string \| null | yes | Their name, when the approver was loaded; null otherwise. | | `data[].decisions[].stakeholder_id` | string \| null | yes | The stakeholder seat a client guest decided from; null for staff. | | `data[].decisions[].verdict` | string | yes | What they said: `approved`, `rejected` or `changes_requested`. | | `data[].decisions[].comment` | string \| null | yes | The reason they gave; null when none. | | `data[].decisions[].signature_hash` | string | yes | SHA-256 over the request, the round, the person, the verdict and the moment, for the audit export. | | `data[].decisions[].decided_at` | string | yes | When the verdict was given, ISO 8601. | | `data[].created_at` | string \| null | yes | When the request was submitted, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the request last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `scope`, `status`, `kind` or `artefact_type` is not one of its values, `project_id` is not a uuid, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Submit a baseline Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-baseline `POST /v1/baselines/{baseline}/submit` ```bash curl -X POST https://api.example.test/v1/baselines/$BASELINE_ID/submit \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"summary": "The scope as agreed on the kick-off call."}' ``` Puts a draft baseline up for the team's approval (`baseline_approval`): it moves into review and `baseline.status_changed` goes out beside `approval.requested`; when that approval carries, the baseline is frozen, every item is pinned to its approved version and locked, the requirements and rules among them move to baselined, and `baseline.frozen` goes out. On a scope baseline the team already approved, the same call asks the client to sign (`client_sign_off`): the baseline moves to awaiting sign-off and the stakeholders who approve requirements, with an account linked to their seat, are the approvers beside anyone the policy names; when they all agree, `baseline.signed_off` goes out. Resubmits as the next round when the open request waits for changes. Answers `201` with the request. - Requires ability: `baseline:freeze` - Fires events: `approval.requested` - MCP tools: `submit_for_review` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `baseline` | path | string (uuid) | yes | The baseline. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) What to tell the approvers when a record is submitted or resubmitted for approval. | Field | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string \| null | no | What changed and why, for the approvers; up to 2000 characters. On a resubmission the earlier words stay when none are given. | ### Responses - **201**: The request, created. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `baseline:freeze`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the baseline is already under review and not waiting for changes, holds no items, cannot be submitted as it stands (a frozen baseline the client already signed, or one of a kind the client does not sign), the gate names nobody, or `summary` is longer than 2000 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Submit a business rule Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-business-rule `POST /v1/business-rules/{rule}/submit` ```bash curl -X POST https://api.example.test/v1/business-rules/$RULE_ID/submit \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"summary": "Worked examples added."}' ``` Puts a business rule up for approval: it moves into review and `business_rule.status_changed` goes out beside `approval.requested`. Resubmits as the next round when the open request waits for changes. Answers `201` with the request. - Requires ability: `business-rule:update` - Fires events: `approval.requested` - MCP tools: `submit_for_review` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The rule. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) What to tell the approvers when a record is submitted or resubmitted for approval. | Field | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string \| null | no | What changed and why, for the approvers; up to 2000 characters. On a resubmission the earlier words stay when none are given. | ### Responses - **201**: The request, created. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `business-rule:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the rule is already under review and not waiting for changes, cannot be submitted as it stands (only a draft or an approved rule can), the gate names nobody, or `summary` is longer than 2000 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Submit a change request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-change-request `POST /v1/change-requests/{changeRequest}/submit` ```bash curl -X POST https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID/submit \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"summary": "Impact worked out with the team; five days and the checkout tests."}' ``` Puts a submitted or analysed change request to its approvers (`change_request`): it moves to awaiting approval and `change_request.status_changed` goes out beside `approval.requested`. The approvers are the ones the policy names plus, when the request names a stakeholder with an account linked to their seat, that stakeholder. When the request carries it is approved and the items of the baseline it names, among the records it touches, are unlocked; a rejection settles it as rejected with the approver's words kept as its decision note; a withdrawal returns it under analysis. Resubmits as the next round when the open request waits for changes. Answers `201` with the request. - Requires ability: `change-request:approve` - Fires events: `approval.requested` - MCP tools: `submit_for_review` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The change request. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) What to tell the approvers when a record is submitted or resubmitted for approval. | Field | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string \| null | no | What changed and why, for the approvers; up to 2000 characters. On a resubmission the earlier words stay when none are given. | ### Responses - **201**: The request, created. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `change-request:approve`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the request is already under review and not waiting for changes, cannot be submitted as it stands (only a submitted or analysed request can), the gate names nobody, or `summary` is longer than 2000 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Submit a requirement Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-requirement `POST /v1/requirements/{requirement}/submit` ```bash curl -X POST https://api.example.test/v1/requirements/$REQUIREMENT_ID/submit \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"summary": "First cut of the voucher rules."}' ``` Puts a requirement up for approval: it moves into review and `requirement.status_changed` goes out beside `approval.requested`. An approved requirement that changed since can be submitted again; its approval then stamps the next version. Resubmits as the next round when the open request waits for changes. Answers `201` with the request. - Requires ability: `requirement:update` - Fires events: `approval.requested` - MCP tools: `submit_for_review` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) What to tell the approvers when a record is submitted or resubmitted for approval. | Field | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string \| null | no | What changed and why, for the approvers; up to 2000 characters. On a resubmission the earlier words stay when none are given. | ### Responses - **201**: The request, created. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `requirement:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the requirement is already under review and not waiting for changes, cannot be submitted as it stands (only a draft or an approved requirement can), the gate names nobody, or `summary` is longer than 2000 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Submit a story Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-story `POST /v1/work-items/{item}/submit` ```bash curl -X POST https://api.example.test/v1/work-items/$ITEM_ID/submit \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"summary": "Acceptance criteria complete."}' ``` Puts a story up for approval. A story keeps its board status throughout: the request is what shows it is under review, and an approval stamps its `version`, `approved_at` and `approved_revision_id` without moving it. Only a story that is not done or cancelled can be submitted; a task, a bug, a spike or an improvement is refused. Resubmits as the next round when the open request waits for changes. Answers `201` with the request. - Requires ability: `work-item:update` - Fires events: `approval.requested` - MCP tools: `submit_for_review` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The story. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) What to tell the approvers when a record is submitted or resubmitted for approval. | Field | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string \| null | no | What changed and why, for the approvers; up to 2000 characters. On a resubmission the earlier words stay when none are given. | ### Responses - **201**: The request, created. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `work-item:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the item is not a story, is closed, is already under review and not waiting for changes, the gate names nobody, or `summary` is longer than 2000 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Submit a use case Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-use-case `POST /v1/use-cases/{useCase}/submit` ```bash curl -X POST https://api.example.test/v1/use-cases/$USE_CASE_ID/submit \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"summary": "Alternate flows written."}' ``` Puts a use case up for approval: it moves into review and `use_case.status_changed` goes out beside `approval.requested`. Resubmits as the next round when the open request waits for changes. Answers `201` with the request. - Requires ability: `use-case:update` - Fires events: `approval.requested` - MCP tools: `submit_for_review` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `useCase` | path | string (uuid) | yes | The use case. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) What to tell the approvers when a record is submitted or resubmitted for approval. | Field | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string \| null | no | What changed and why, for the approvers; up to 2000 characters. On a resubmission the earlier words stay when none are given. | ### Responses - **201**: The request, created. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `use-case:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the use case is already under review and not waiting for changes, cannot be submitted as it stands (only a draft or an approved use case can), the gate names nobody, or `summary` is longer than 2000 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Submit an epic Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-an-epic `POST /v1/epics/{epic}/submit` ```bash curl -X POST https://api.example.test/v1/epics/$EPIC_ID/submit \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"summary": "Scope agreed with the sponsor on Tuesday."}' ``` Puts the epic up for approval: it moves into review, the approvers are resolved from the project's policy for requirement reviews, each is notified, and `approval.requested` goes out with the epic's own `epic.status_changed`. A project whose gate is off approves the epic at once, announced as an approval that carried by itself. When the epic's open request is waiting for changes, the same call resubmits it as the next round with the words given (or the earlier words when none are). Answers `201` with the request. - Requires ability: `epic:update` - Fires events: `approval.requested` - MCP tools: `submit_for_review` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `epic` | path | string (uuid) | yes | The epic. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) What to tell the approvers when a record is submitted or resubmitted for approval. | Field | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string \| null | no | What changed and why, for the approvers; up to 2000 characters. On a resubmission the earlier words stay when none are given. | ### Responses - **201**: The request, created. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `epic:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the epic is already under review and not waiting for changes, cannot be submitted as it stands (only a draft or an approved epic can), the gate names nobody, or `summary` is longer than 2000 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a project's approval policy Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/update-a-projects-approval-policy `PUT /v1/projects/{project}/approval-policies` ```bash curl -X PUT https://api.example.test/v1/projects/$PROJECT_ID/approval-policies \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"kind": "requirement_review", "rule": "quorum", "quorum": 2, "approver_roles": ["project_manager", "business_analyst"], "sla_hours": 48}' ``` Writes the project's policy for one kind in full; a field left out takes the kind's default. Requests already open keep the rule and the approvers they were submitted with. Answers `200` with the policy as stored and emits `approval.policy_updated` with what changed against the policy that applied before; nothing is emitted when nothing differs. - Requires ability: `approval:manage-policies` - Fires events: `approval.policy_updated` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) How the project decides one kind of approval. The whole policy is written at once; a field left out takes the default for that kind. | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | yes | The gate the policy decides: `requirement_review` today. A kind whose module has not landed is refused. | | `rule` | `any`, `all`, `quorum` | no | How many approvals carry a request: `any`, `all` or `quorum`. | | `quorum` | integer | no | The quorum under the quorum rule; at least 1. | | `approver_roles` | array of `owner`, `project_manager`, `business_analyst`, `developer`, `qa_engineer`, `release_manager`, `viewer`, `guest` | no | The project roles whose holders approve: `owner`, `project_manager`, `business_analyst`, `developer`, `qa_engineer`, `release_manager` or `viewer`; never `guest`. | | `approver_user_ids` | array of string (uuid) | no | The people named by id; each must be a member of the team. | | `approver_stakeholder_ids` | array of string (uuid) | no | The stakeholders named by id; each must be one of the project's, and only a seat with a linked account can decide. | | `sla_hours` | integer \| null | no | Hours the approvers have before the request is overdue, 1 to 720; null for no clock. | | `reminder_hours` | integer \| null | no | Hours after which approvers who have not answered are nudged, 1 to 720; null for none. | | `auto_approve_after_hours` | integer \| null | no | Hours after which an unanswered request carries by itself, 1 to 720; null for never. | | `requires_comment_on_reject` | boolean | no | Whether a rejection or a request for changes needs a reason. | | `enabled` | boolean | no | Whether the gate is on. An enabled gate must name at least one approver; off, a submission carries at once. | ### Responses - **200**: The policy as stored. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.kind` | string | yes | The gate: `requirement_review` today; the other kinds join with their modules. | | `data.rule` | string | yes | How many approvals carry a request: `any`, `all` or `quorum`. | | `data.quorum` | integer | yes | The quorum under the quorum rule; at least 1. | | `data.approver_roles` | array of any | yes | The project roles whose holders approve: `owner`, `project_manager`, `business_analyst`, `developer`, `qa_engineer`, `release_manager` or `viewer`. | | `data.approver_user_ids` | array of string | yes | The people named by id; each must be a member of the team. | | `data.approver_stakeholder_ids` | array of string | yes | The stakeholders named by id; only a seat with a linked account can decide. | | `data.sla_hours` | integer \| null | yes | Hours the approvers have before the request is overdue; null for no clock. | | `data.reminder_hours` | integer \| null | yes | Hours after which approvers who have not answered are nudged; null for none. | | `data.auto_approve_after_hours` | integer \| null | yes | Hours after which an unanswered request carries by itself; null for never. | | `data.requires_comment_on_reject` | boolean | yes | Whether a rejection or a request for changes needs a reason. | | `data.enabled` | boolean | yes | Whether the gate is on; off, a submission carries at once. | | `data.stored` | boolean | yes | Whether the project saved this policy; false while it reads the defaults. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `approval:manage-policies`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `kind` is missing, unknown or not available yet, a role is `guest`, a named person is not a member of the team, a named stakeholder is not the project's, an enabled gate names nobody, or a number is out of its range. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Withdraw an approval Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/withdraw-an-approval `POST /v1/approvals/{approval}/withdraw` ```bash curl -X POST https://api.example.test/v1/approvals/$APPROVAL_ID/withdraw \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Takes an open request back, by the person who raised it or by someone whose role on the project carries `approval:withdraw`; the record returns to where it stood and `approval.withdrawn` goes out with the record's own status event. Answers `200` with the request, withdrawn. - Requires ability: `approval:withdraw` - Fires events: `approval.withdrawn` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `approval` | path | string (uuid) | yes | The request. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The request, withdrawn. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or its holder neither raised the request nor holds `approval:withdraw` on the project. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the request is not open. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete an attachment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/delete-an-attachment `DELETE /v1/attachments/{attachment}` ```bash curl -X DELETE https://api.example.test/v1/attachments/$ATTACHMENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a file the token's holder uploaded, or anyone's when the holder's role carries `attachment:delete`, bytes and record. Returns `204 No Content` and emits `attachment.deleted`. - Requires ability: `attachment:delete` - Fires events: `attachment.deleted` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `attachment` | path | string (uuid) | yes | The attachment, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the file is someone else's and the role does not carry `attachment:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the attachment is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Download an attachment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/download-an-attachment `GET /v1/attachments/{attachment}/download` ```bash curl -L https://api.example.test/v1/attachments/$ATTACHMENT_ID/download \ -H "Authorization: Bearer $TOKEN" \ -o checkout-wireframe.png ``` Streams the file's bytes with its name and MIME type. The attachment must be on an artefact the token may see. - Requires ability: `attachment:view` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `attachment` | path | string (uuid) | yes | The attachment, bound by the middleware. | ### Responses - **200**: The file. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `attachment:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the attachment is of a project the token may not see, on an artefact hidden from it, or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Attachments API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments An **attachment** is a file pinned on one artefact of a project: a wireframe on a story, a signed specification on a requirement, a sample export on a data entity, a vendor quote on a decision. The record keeps the file's name, size, MIME type and kind, who uploaded it and when; the bytes live on a private disk and leave it only through the download endpoint, to a token that may read the artefact, never through a URL anyone can guess. Nine artefact types carry attachments today: epics, work items, requirements, business rules, use cases, RAID items, decisions, data entities and glossary terms. An attachment is not versioned: it is added or removed, and both are announced. A token reads the attachments of the artefacts its holder may see, a client guest's token those on the agreed record. Uploading needs `attachment:create` on the project; the uploader removes their own files, `attachment:delete` anyone's. Images, PDFs, Office documents, text, CSV, JSON and ZIP files are accepted, up to 25 MB each unless the operator raised the ceiling. ## Endpoints - `GET /v1/projects/{project}/attachments` — [List an artefact's attachments](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/list-an-artefacts-attachments) - `POST /v1/projects/{project}/attachments` — [Upload an attachment](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/upload-an-attachment) - `GET /v1/attachments/{attachment}/download` — [Download an attachment](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/download-an-attachment) - `DELETE /v1/attachments/{attachment}` — [Delete an attachment](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/delete-an-attachment) ## Related - [Comments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments): The words beside the files. - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items): [requirements](/api/v1/reference/requirements), [business rules](/api/v1/reference/business-rules), [use cases](/api/v1/reference/use-cases), [the RAID log](/api/v1/reference/raid-log) and [decisions](/api/v1/reference/decisions): the artefacts the files hang on. - [attachment.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment): The files pinned on a project's artefacts: a file uploaded and a file removed. --- # List an artefact's attachments Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/list-an-artefacts-attachments `GET /v1/projects/{project}/attachments` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/attachments?artefact_type=work-item&artefact_id=$WORK_ITEM_ID" \ -H "Authorization: Bearer $TOKEN" ``` The files pinned on one artefact, newest first, each with its uploader's name and the endpoint that streams it. `artefact_type` and `artefact_id` are required; the artefact must be one the token may see. - Requires ability: `attachment:view-any` - MCP tools: `list_attachments` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `artefact_type` | query | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. Required. | | `artefact_id` | query | string (uuid) | yes | The artefact's id. Required. | ### Responses - **200**: Array of `AttachmentResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The attachment's id. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].artefact_type` | string | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data[].artefact_id` | string | yes | The artefact's id. | | `data[].name` | string | yes | The file's name as uploaded. | | `data[].size` | integer | yes | The file's size in bytes. | | `data[].mime` | string | yes | The MIME type the server detected. | | `data[].kind` | string | yes | What kind of file it is: `image`, `document`, `spreadsheet`, `presentation`, `archive`, `text` or `other`. | | `data[].uploaded_by` | string \| null | yes | The person who uploaded it; null when the account is gone. | | `data[].uploader_name` | string \| null | yes | The uploader's name, when the uploader was loaded; null otherwise. | | `data[].download_url` | string | yes | The endpoint that streams the bytes, with the same token. | | `data[].created_at` | string \| null | yes | When the file was uploaded, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `attachment:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the artefact is hidden from the token, missing or of another project. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `artefact_type` is not one of the nine or `artefact_id` is not a uuid. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Upload an attachment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/upload-an-attachment `POST /v1/projects/{project}/attachments` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/attachments \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -F "artefact_type=work-item" \ -F "artefact_id=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \ -F "file=@checkout-wireframe.png" ``` Stores a file sent as `multipart/form-data` and pins it on an artefact of the project as the token's holder. The file's name is kept; its kind is read from its type. Answers `201` with the attachment and emits `attachment.created`. - Requires ability: `attachment:create` - Fires events: `attachment.created` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (multipart/form-data) A file to pin on an artefact of the project, sent as `multipart/form-data`: the artefact by type and id and the file itself. Images, PDFs, Office documents, text, CSV, JSON and ZIP files are accepted, up to the project's size ceiling (25 MB unless the operator raised it). | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `artefact_id` | string (uuid) | yes | The artefact's id, one of the project's. | | `file` | string (binary) | yes | The file, as a multipart part. Its name is kept as the attachment's name. | ### Responses - **201**: The attachment, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The attachment's id. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.artefact_type` | string | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data.artefact_id` | string | yes | The artefact's id. | | `data.name` | string | yes | The file's name as uploaded. | | `data.size` | integer | yes | The file's size in bytes. | | `data.mime` | string | yes | The MIME type the server detected. | | `data.kind` | string | yes | What kind of file it is: `image`, `document`, `spreadsheet`, `presentation`, `archive`, `text` or `other`. | | `data.uploaded_by` | string \| null | yes | The person who uploaded it; null when the account is gone. | | `data.uploader_name` | string \| null | yes | The uploader's name, when the uploader was loaded; null otherwise. | | `data.download_url` | string | yes | The endpoint that streams the bytes, with the same token. | | `data.created_at` | string \| null | yes | When the file was uploaded, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `attachment:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the artefact is hidden from the token, missing or of another project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the type is unknown, the id is not a uuid, the file is missing, over the size ceiling or of a kind that is not accepted. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a baseline Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/add-a-baseline `POST /v1/projects/{project}/baselines` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/baselines \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"name": "v1.0 scope", "kind": "scope", "description": "

Everything the client signs for the first release.

"}' ``` Only `name` is required, unique within the project regardless of case. The baseline starts as an empty draft of the kind given (`scope` unless `kind` says otherwise); its records are set with the items endpoint. The save writes version one of the baseline. Answers `201` with the baseline and emits `baseline.created`. - Requires ability: `baseline:create` - Fires events: `baseline.created` - MCP tools: `create_baseline` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A baseline to add or change. Only `name` is required on a create; the kind defaults to `scope`. On an update, a field left out keeps its value and a field sent as null is cleared. The records it holds are set through the items endpoint; the status moves through the approval engine and the status endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | What the baseline is called, 3 to 120 characters, unique within the project. | | `kind` | `scope`, `release`, `sprint`, `milestone` | no | The kind: `scope`, `release`, `sprint` or `milestone`. Defaults to `scope`, which the client signs after the team approved it. | | `description` | string \| null | no | What the baseline marks, as HTML; filtered to a safe subset when read. | ### Responses - **201**: The baseline, empty, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `baseline:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed or the project already has a baseline of that name. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a baseline Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/delete-a-baseline `DELETE /v1/baselines/{baseline}` ```bash curl -X DELETE https://api.example.test/v1/baselines/$BASELINE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a draft baseline with its items, its versions and its activity history. A baseline past its draft is the record: supersede it instead. Returns `204 No Content` and emits `baseline.deleted`. - Requires ability: `baseline:delete` - Fires events: `baseline.deleted` - MCP tools: `delete_baseline` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `baseline` | path | string (uuid) | yes | The baseline, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `baseline:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the baseline is no longer a draft. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a baseline Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-baseline `GET /v1/baselines/{baseline}` ```bash curl https://api.example.test/v1/baselines/$BASELINE_ID \ -H "Authorization: Bearer $TOKEN" ``` One baseline with every record it holds. A baseline of a project the token may not see, or a draft read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `kind` is `scope`, `release`, `sprint` or `milestone`; `status` is `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off` or `superseded`; `frozen` says whether the record is read-only and `locks_items` whether it still holds its items against edits. - Requires ability: `baseline:view` - MCP tools: `get_baseline` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `baseline` | path | string (uuid) | yes | The baseline, bound by the middleware. | ### Responses - **200**: The baseline with its items. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a version of a baseline Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-version-of-a-baseline `GET /v1/baselines/{baseline}/revisions/{revision}` ```bash curl https://api.example.test/v1/baselines/$BASELINE_ID/revisions/$REVISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One version of the baseline. A version of another artefact or a version the token may not see is `404 RESOURCE_NOT_FOUND`. - Requires ability: `baseline:view` - MCP tools: `get_baseline` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `baseline` | path | string (uuid) | yes | The baseline, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Baselines API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines A **baseline** is a named set of approved records frozen together: the scope the client signs before the build starts, what a release ships, what a sprint commits to, or the record as it stood at a milestone. A baseline is composed while it is a draft, from epics, stories, requirements, business rules and use cases that are already approved; submitted for the team's approval, it is frozen the moment that approval carries: every item is pinned to its approved version, the requirements and rules among them move to baselined, and nothing it holds changes again except through an approved change request. A scope baseline then passes a second gate, the client's sign-off, answered by the stakeholders who approve requirements. Every save of a draft baseline writes a **version** with its items inside; the versions are read and put back through the endpoints below, under the baselines' own abilities. A frozen baseline is the record and is never restored over. A staff token sees the baselines of the projects it may see; a client guest's token sees the frozen ones (approved, awaiting sign-off, signed off or superseded) and none of the drafts, and a draft it asks for is a `404 RESOURCE_NOT_FOUND`. Every write runs the same action the Baselines page runs, under the project permissions, and announces itself as a `baseline.*` event. Submitting a baseline for approval and asking the client to sign are on the [approvals endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals). ## Endpoints - `GET /v1/baselines/{baseline}` — [Get a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-baseline) - `PATCH /v1/baselines/{baseline}` — [Update a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/update-a-baseline) - `DELETE /v1/baselines/{baseline}` — [Delete a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/delete-a-baseline) - `GET /v1/baselines/{baseline}/revisions` — [List a baseline's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-a-baselines-versions) - `GET /v1/baselines/{baseline}/revisions/{revision}` — [Get a version of a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-version-of-a-baseline) - `GET /v1/projects/{project}/baselines` — [List baselines](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-baselines) - `POST /v1/projects/{project}/baselines` — [Add a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/add-a-baseline) - `PUT /v1/baselines/{baseline}/items` — [Set a baseline's items](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/set-a-baselines-items) - `POST /v1/baselines/{baseline}/status` — [Move a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/move-a-baseline) - `POST /v1/baselines/{baseline}/revisions/{revision}/restore` — [Restore a version of a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/restore-a-version-of-a-baseline) ## Related - [Change Requests API](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests): The one door through which a frozen baseline's items are unlocked. - [Approvals API](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals): The team's approval and the client's sign-off a baseline passes. - [baseline.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline): The life of a project's baselines: a baseline added as a draft, its words or items changed, moved by hand, frozen by the team's approval, signed by the client, put back to an earlier version and deleted. --- # List a baseline's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-a-baselines-versions `GET /v1/baselines/{baseline}/revisions` ```bash curl "https://api.example.test/v1/baselines/$BASELINE_ID/revisions" \ -H "Authorization: Bearer $TOKEN" ``` Every version of the baseline, newest first, 25 per page (`per_page` 1 to 100). Each carries its `version` number, the `reason` it was written, who wrote it (`author_id`, `author_name`) and through what (`actor_kind`), a one-line `summary`, the `changed_fields`, the `diff` from the version before and the full `snapshot`, the items included under `items`. - Requires ability: `baseline:view` - MCP tools: `get_baseline` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `baseline` | path | string (uuid) | yes | The baseline, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role does not carry `baseline:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List baselines Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-baselines `GET /v1/projects/{project}/baselines` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/baselines?status=signed_off" \ -H "Authorization: Bearer $TOKEN" ``` The baselines of one project in the project's order, 25 per page (`per_page` 1 to 100), each with the count of records it holds. `sort_by` takes `name`, `kind`, `status`, `frozen_at`, `signed_off_at`, `position`, `created_at` or `updated_at`. `kind` and `status` narrow the page to one value each, and `search` to the baselines whose name contains the words. A client guest's token receives the frozen baselines only. - Requires ability: `baseline:view-any` - MCP tools: `list_baselines` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `kind` | query | `scope`, `release`, `sprint`, `milestone` \| null | no | Narrow to one kind: `scope`, `release`, `sprint` or `milestone`. An unknown kind is refused. | | `status` | query | `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off`, `superseded` \| null | no | Narrow to one status: `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off` or `superseded`. | | `search` | query | string \| null | no | Words the name must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of baselines, each with its item count. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The baseline's id. | | `data[].project_id` | string | yes | The project the baseline belongs to. | | `data[].name` | string | yes | What the baseline is called, unique within the project. | | `data[].kind` | string | yes | The kind: `scope`, `release`, `sprint` or `milestone`. A scope baseline is signed by the client after the team approved it. | | `data[].description` | string \| null | yes | What the baseline marks, as HTML filtered to a safe subset; null when none. | | `data[].status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off` or `superseded`. | | `data[].frozen` | boolean | yes | Whether the baseline is frozen: approved by the team and read-only from then on. | | `data[].locks_items` | boolean | yes | Whether the baseline still holds its items against edits; false once superseded. | | `data[].version` | integer | yes | Its approved version: 0 while never approved, 1 once the team approved it, 2 once the client signed. | | `data[].approved_at` | string \| null | yes | When the last approval carried, ISO 8601; null until then. | | `data[].approved_revision_id` | string \| null | yes | The revision of the baseline the last approval froze; null until then. | | `data[].frozen_at` | string \| null | yes | When the team's approval froze it, ISO 8601; null while a draft or in review. | | `data[].frozen_by` | string \| null | yes | The person whose approval froze it; null until frozen or when the policy carried it by itself. | | `data[].signed_off_at` | string \| null | yes | When the client signed a scope baseline, ISO 8601; null otherwise. | | `data[].item_count` | integer | no | How many records it holds, when the count was loaded. | | `data[].items` | array of object | no | The records it holds, in order, when they were loaded. | | `data[].items[].id` | string | yes | The item's id. | | `data[].items[].baseline_id` | string | yes | The baseline that holds it. | | `data[].items[].artefact_type` | string | yes | The record's family: `epic`, `work-item`, `requirement`, `business-rule` or `use-case`. | | `data[].items[].artefact_id` | string | yes | The record's id. | | `data[].items[].reference` | string | yes | The record's key as it read when added, such as `CRM-FR-12`. | | `data[].items[].title` | string | yes | The record's title as it read when added. | | `data[].items[].version` | integer | yes | The record's approved version the baseline holds. | | `data[].items[].revision_id` | string \| null | yes | The revision the freeze pinned; null until frozen or when the record had none. | | `data[].items[].frozen_at` | string \| null | yes | When the baseline froze it, ISO 8601; null while the baseline is a draft. | | `data[].items[].locked` | boolean | yes | Whether the record is held right now: frozen, not unlocked, under a baseline that still locks its items. | | `data[].items[].unlocked_at` | string \| null | yes | When an approved change request released it, ISO 8601; null while held or never frozen. | | `data[].items[].unlocked_by_change_request_id` | string \| null | yes | The change request that released it; null otherwise. | | `data[].items[].position` | integer | yes | Its place in the baseline, from zero. | | `data[].position` | integer | yes | Its place among the project's baselines, from zero. | | `data[].created_at` | string \| null | yes | When the baseline was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the baseline last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `baseline:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `kind` or `status` is not one of its values, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a baseline Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/move-a-baseline `POST /v1/baselines/{baseline}/status` ```bash curl -X POST https://api.example.test/v1/baselines/$BASELINE_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "superseded"}' ``` Moves a baseline by hand, which is only ever to `superseded`, from approved or signed off, when a newer baseline replaces it: the record stays readable and its items are no longer held by it. Every other move (into review, approved, awaiting sign-off, signed off) is the approval engine's and is refused here. The move writes a version with the reason `transition`. Answers `200` with the baseline and emits `baseline.status_changed` with the status before and after; a move to the status the baseline is in already answers `200` and emits nothing. - Requires ability: `baseline:update` - Fires events: `baseline.status_changed` - MCP tools: `update_baseline` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `baseline` | path | string (uuid) | yes | The baseline, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the baseline to by hand, which is only `superseded`: the moves into review, approved, awaiting sign-off and signed off are the approval engine's. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off`, `superseded` | yes | Where the baseline goes: `superseded`, from approved or signed off. Any other status is the approval engine's and is refused here. | ### Responses - **200**: The baseline, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `baseline:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, the lifecycle refuses the move, or the move is the approval engine's. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a baseline Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/restore-a-version-of-a-baseline `POST /v1/baselines/{baseline}/revisions/{revision}/restore` ```bash curl -X POST https://api.example.test/v1/baselines/$BASELINE_ID/revisions/$REVISION_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the version's captured columns and items back onto a draft baseline and saves. The save writes a new version with the reason `restore` that names the one put back in `restored_from_id`; nothing earlier is rewritten. A frozen baseline is the record and is never restored over. Answers `201` with the version written and emits `baseline.restored` with both version numbers. - Requires ability: `baseline:update` - Fires events: `baseline.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `baseline` | path | string (uuid) | yes | The baseline, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version the restore wrote, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `baseline:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the version is not the baseline's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the version is identical to the current record, or the baseline is frozen. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Set a baseline's items Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/set-a-baselines-items `PUT /v1/baselines/{baseline}/items` ```bash curl -X PUT https://api.example.test/v1/baselines/$BASELINE_ID/items \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"items": [{"type": "requirement", "id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13"}, {"type": "work-item", "id": "c0d1e2f3-a4b5-4c6d-9e7f-8a9b0c1d2e35"}]}' ``` Replaces the records a draft baseline holds with the list sent, in that order: records left out are dropped, new ones added with the reference, title and approved version as they stand, and the rest kept. Every record must be approved (its version at least one), the project's own, and of a family a baseline holds: `epic`, `work-item`, `requirement`, `business-rule` or `use-case`. An empty list empties the baseline. The baseline's own version captures the new list. Answers `200` with the baseline and its items and emits `baseline.updated` with `changes.items` carrying the references before and after; a list that leaves the items as they were answers `200` and emits nothing. - Requires ability: `baseline:update` - Fires events: `baseline.updated` - MCP tools: `update_baseline` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `baseline` | path | string (uuid) | yes | The baseline, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The records a draft baseline holds, in full: the list sent replaces the list held. Every record must be approved and the project's own; a record of a family a baseline cannot hold is refused. | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The records, in the order wanted, up to 500; an empty list empties the baseline. | | `items[].type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The record's family: `epic`, `work-item`, `requirement`, `business-rule` or `use-case`. | | `items[].id` | string (uuid) | yes | The record's id. | ### Responses - **200**: The baseline with its items. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `baseline:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when a record is hidden, gone or another project's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `items` is missing, a row lacks its type or id, a type is not one a baseline holds, a record is not approved yet, or the baseline is no longer a draft. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a baseline Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/update-a-baseline `PATCH /v1/baselines/{baseline}` ```bash curl -X PATCH https://api.example.test/v1/baselines/$BASELINE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"description": "

The scope as agreed on the kick-off call.

"}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. Only a draft changes; a baseline in review or frozen is the record and answers `422`. A save that changes a captured column writes a new version. Answers `200` with the baseline and emits `baseline.updated` with the columns that changed, each with its old and new value; an update that changes no column answers `200` and emits nothing. - Requires ability: `baseline:update` - Fires events: `baseline.updated` - MCP tools: `update_baseline` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `baseline` | path | string (uuid) | yes | The baseline, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A baseline to add or change. Only `name` is required on a create; the kind defaults to `scope`. On an update, a field left out keeps its value and a field sent as null is cleared. The records it holds are set through the items endpoint; the status moves through the approval engine and the status endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | What the baseline is called, 3 to 120 characters, unique within the project. | | `kind` | `scope`, `release`, `sprint`, `milestone` | no | The kind: `scope`, `release`, `sprint` or `milestone`. Defaults to `scope`, which the client signs after the team approved it. | | `description` | string \| null | no | What the baseline marks, as HTML; filtered to a safe subset when read. | ### Responses - **200**: The baseline. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `baseline:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, the name is taken, or the baseline is no longer a draft. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a business rule Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/add-a-business-rule `POST /v1/projects/{project}/business-rules` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/business-rules \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Platinum tier threshold", "type": "inference", "statement": "

A member whose spend over the last twelve months reaches 5,000 is a platinum member.

", "condition": "When a member'"'"'s rolling twelve-month spend reaches 5,000", "action": "then the member moves to the platinum tier on the first of the next month", "examples": [ { "input": "Spend 4,999 on 30 November", "expected": "Stays gold" }, { "input": "Spend 5,000 on 30 November", "expected": "Platinum from 1 December" } ], "priority": "must" }' ``` Only `name` is required. The rule takes the next number of the rules' sequence and the reference spelled from the project's key, starts as a `draft` unless `status` says otherwise, a `constraint` unless `type` does and `should` unless `priority` does. The dates in force must be in order, the authority stakeholder must be a stakeholder of the project and the owner a member of the team. The save writes version one. Answers `201` with the rule and emits `business_rule.created`. - Requires ability: `business-rule:create` - Fires events: `business_rule.created` - MCP tools: `propose_business_rule` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A business rule to add or change. Only `name` is required on a create; the kind defaults to `constraint`, the priority to `should` and the status to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; `examples`, when sent, replaces the list; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | What the rule is called, 3 to 200 characters. | | `type` | `constraint`, `derivation`, `computation`, `inference`, `action_enabler`, `policy` | no | The kind: `constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`. Defaults to `constraint`. | | `statement` | string \| null | no | The rule in plain words, as HTML; filtered to a safe subset when read. | | `condition` | string \| null | no | When the rule applies, in one line up to 500 characters. | | `action` | string \| null | no | What follows when it does, in one line up to 500 characters. | | `exceptions` | string \| null | no | When the rule does not apply, as HTML. | | `examples` | array \| null | no | Worked examples, up to twenty rows of `input` (required) and `expected`. Sent, the list is replaced; left out, it stays as it is. | | `examples[].input` | string | yes | The input of a worked example, up to 500 characters; required. | | `examples[].expected` | string \| null | no | What the rule gives for that input, up to 500 characters. | | `authority_stakeholder_id` | string \| null (uuid) | no | The id of the stakeholder who may change the rule, a stakeholder of the same project; null for none. | | `authority` | string \| null | no | Who or what the rule's authority is, in words, up to 300 characters. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority: `must`, `should`, `could` or `wont`. Defaults to `should`. | | `effective_from` | string \| null (date) | no | When the rule comes into force, `Y-m-d`. | | `effective_to` | string \| null (date) | no | When it stops applying, `Y-m-d`; on or after the start. | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | no | Where it starts, on a create only: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. Defaults to `draft`. | | `owner_id` | string \| null (uuid) | no | The id of the person who answers for it, a member of the team; null for nobody. | ### Responses - **201**: The rule, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The rule's id. | | `data.project_id` | string | yes | The project the rule belongs to. | | `data.number` | integer | yes | Its number in the rules' own sequence. | | `data.reference` | string | yes | The key requirements, stories, test cases and people use, such as `CRM-BR-7`. | | `data.name` | string | yes | What the rule is called. | | `data.type` | string | yes | The kind: `constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`. | | `data.statement` | string \| null | yes | The rule in plain words, as HTML filtered to a safe subset; null when none. | | `data.condition` | string \| null | yes | When the rule applies, in one line; null when none. | | `data.action` | string \| null | yes | What follows when it does, in one line; null when none. | | `data.exceptions` | string \| null | yes | When the rule does not apply, as HTML; null when none. | | `data.examples` | array of object | yes | Worked examples in order, each `{ "input": …, "expected": … }`; empty when none. | | `data.examples[].input` | string | yes | The input of a worked example. | | `data.examples[].expected` | string | yes | What the rule gives for that input; empty when not written. | | `data.authority_stakeholder_id` | string \| null | yes | The stakeholder who may change the rule; null when none. | | `data.authority_stakeholder_name` | string \| null | yes | That stakeholder's name, when the authority was loaded; null otherwise. | | `data.authority` | string \| null | yes | Who or what the rule's authority is, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.effective_from` | string \| null | yes | When the rule comes into force, `Y-m-d`; null when always. | | `data.effective_to` | string \| null | yes | When it stops applying, `Y-m-d`; null when open-ended. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's rules, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the rule was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the rule last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `business-rule:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when an example has no input, when the rule would stop applying before it comes into force, when the authority is not a stakeholder of the project, or when the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a business rule Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/delete-a-business-rule `DELETE /v1/business-rules/{rule}` ```bash curl -X DELETE https://api.example.test/v1/business-rules/$RULE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a rule with its versions and its activity history. The number it took is never handed out again. Returns `204 No Content` and emits `business_rule.deleted`. Prefer a move to `deprecated` for a rule the business withdrew; it keeps the record. - Requires ability: `business-rule:delete` - Fires events: `business_rule.deleted` - MCP tools: `delete_business_rule` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The rule, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `business-rule:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a business rule Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-business-rule `GET /v1/business-rules/{rule}` ```bash curl https://api.example.test/v1/business-rules/$RULE_ID \ -H "Authorization: Bearer $TOKEN" ``` One rule with its owner's and its authority stakeholder's names and its worked examples in order. A rule of a project the token may not see, or a draft read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `type` is `constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`; `status` is `draft`, `in_review`, `approved`, `baselined` or `deprecated`; `priority` is `must`, `should`, `could` or `wont`. - Requires ability: `business-rule:view` - MCP tools: `get_business_rule` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The rule, bound by the middleware. | ### Responses - **200**: The rule. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The rule's id. | | `data.project_id` | string | yes | The project the rule belongs to. | | `data.number` | integer | yes | Its number in the rules' own sequence. | | `data.reference` | string | yes | The key requirements, stories, test cases and people use, such as `CRM-BR-7`. | | `data.name` | string | yes | What the rule is called. | | `data.type` | string | yes | The kind: `constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`. | | `data.statement` | string \| null | yes | The rule in plain words, as HTML filtered to a safe subset; null when none. | | `data.condition` | string \| null | yes | When the rule applies, in one line; null when none. | | `data.action` | string \| null | yes | What follows when it does, in one line; null when none. | | `data.exceptions` | string \| null | yes | When the rule does not apply, as HTML; null when none. | | `data.examples` | array of object | yes | Worked examples in order, each `{ "input": …, "expected": … }`; empty when none. | | `data.examples[].input` | string | yes | The input of a worked example. | | `data.examples[].expected` | string | yes | What the rule gives for that input; empty when not written. | | `data.authority_stakeholder_id` | string \| null | yes | The stakeholder who may change the rule; null when none. | | `data.authority_stakeholder_name` | string \| null | yes | That stakeholder's name, when the authority was loaded; null otherwise. | | `data.authority` | string \| null | yes | Who or what the rule's authority is, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.effective_from` | string \| null | yes | When the rule comes into force, `Y-m-d`; null when always. | | `data.effective_to` | string \| null | yes | When it stops applying, `Y-m-d`; null when open-ended. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's rules, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the rule was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the rule last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a version of a business rule Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-version-of-a-business-rule `GET /v1/business-rules/{rule}/revisions/{revision}` ```bash curl https://api.example.test/v1/business-rules/$RULE_ID/revisions/$REVISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One version of the rule. A version of another artefact, a version the token may not see or a client guest's token is `404 RESOURCE_NOT_FOUND`. - Requires ability: `business-rule:view` - MCP tools: `get_business_rule` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The rule, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Business Rules API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules A **business rule** is a decision of the business the system must honour: a limit it refuses to cross, a fact derived from others, a number worked out from others, a conclusion drawn when conditions hold, an action it takes when they do, or a standing policy. Every rule numbers on the rules' own sequence (`CRM-BR-7`) whatever its kind, and carries the rule in plain words, the condition it fires on and what follows, the exceptions, worked examples of an input and what is expected, who has the authority to change it, its MoSCoW priority, the dates it is in force, who answers for it and where it stands, from draft through review to approved, baselined or deprecated. Every save of a rule writes a **version**. The versions are read and put back through the endpoints below, under the rule's own abilities. The reads here see the rules of the projects the token may see; a client guest's token sees the approved and baselined ones only, because a draft is the analyst's working note until the client signs it off. A rule outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Business Rules page runs, under the project permissions, and announces itself as a `business_rule.*` event. ## Endpoints - `GET /v1/business-rules/{rule}` — [Get a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-business-rule) - `PATCH /v1/business-rules/{rule}` — [Update a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/update-a-business-rule) - `DELETE /v1/business-rules/{rule}` — [Delete a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/delete-a-business-rule) - `GET /v1/business-rules/{rule}/revisions` — [List a business rule's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-versions) - `GET /v1/business-rules/{rule}/revisions/{revision}` — [Get a version of a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-version-of-a-business-rule) - `GET /v1/business-rules/{rule}/quality-reviews` — [List a business rule's quality reviews](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-quality-reviews) - `POST /v1/business-rules/{rule}/quality-reviews` — [Review a business rule's quality](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/review-a-business-rules-quality) - `GET /v1/projects/{project}/business-rules` — [List business rules](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-business-rules) - `POST /v1/projects/{project}/business-rules` — [Add a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/add-a-business-rule) - `POST /v1/business-rules/{rule}/status` — [Move a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/move-a-business-rule) - `POST /v1/business-rules/{rule}/revisions/{revision}/restore` — [Restore a version of a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/restore-a-version-of-a-business-rule) ## Related - [Requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements): The requirements the rules constrain, once links land. - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders): The people a rule's authority can be. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): Who changed a rule and when, beside the versions themselves. - [business_rule.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule): The lifecycle of a project's business rules: a rule added, changed, moved along its review lifecycle, put back to an earlier version and deleted. --- # List a business rule's quality reviews Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-quality-reviews `GET /v1/business-rules/{rule}/quality-reviews` ```bash curl https://api.example.test/v1/business-rules/{rule}/quality-reviews \ -H "Authorization: Bearer $TOKEN" ``` Every review Heimdall wrote of the business rule, newest first, each with the version it read, its score and band, its summary and its findings. Compare the first review's `version` with the business rule's current version from its history: a review of an older version may be stale. A client guest's token is refused. - Requires ability: `business-rule:view` - MCP tools: `list_quality_reviews` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The business rule, bound by the middleware. | ### Responses - **200**: Array of `QualityReviewResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The review's id. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].artefact_type` | string | yes | The artefact's type: `work-item`, `requirement` or `business-rule`. | | `data[].artefact_id` | string | yes | The artefact's id. | | `data[].version` | integer \| null | yes | The version of the artefact Heimdall read, as its history numbers them; null when the artefact had no version yet. | | `data[].score` | integer | yes | 0 to 100: 100 less each finding's penalty (25 for `high`, 10 for `medium`, 4 for `low`), never below 0. | | `data[].band` | string | yes | What the score means: `ready` at 80 and above, `needs_work` from 50 to 79, `not_ready` below 50. | | `data[].summary` | string \| null | yes | Two or three sentences on the record as a whole; null when the model gave none. | | `data[].findings` | array of object | yes | The findings, the most serious first; empty when the record was found sound. | | `data[].findings[].code` | string | yes | The kind of problem: `independent`, `negotiable`, `valuable`, `estimable`, `small` or `testable` for an INVEST breach, `ambiguous`, `missing_criteria`, `untestable`, `missing_measure`, `missing_rationale`, `missing_examples`, `incomplete`, `rule_conflict`, `duplicate`, `stack_drift` or `other`. | | `data[].findings[].severity` | string | yes | How much it matters: `high` (a developer or a tester would be blocked or misled), `medium` (usable but would be argued over) or `low` (polish). | | `data[].findings[].message` | string | yes | What is wrong or missing, quoting the words concerned. | | `data[].findings[].suggestion` | string \| null | yes | What to write instead, in the record's own words; null when there is nothing to suggest. | | `data[].findings[].field` | string \| null | yes | The field concerned, such as `title`, `description`, `criteria`, `statement` or `measure`; null for the whole record. | | `data[].findings_count` | integer | yes | How many findings the review carries. | | `data[].provider` | string | yes | The provider that answered, by its configuration key. | | `data[].model` | string | yes | The model that answered. | | `data[].usage_id` | string \| null | yes | The ledger row that filed the call's cost; null when that row is gone. | | `data[].reviewed_by` | string \| null | yes | The person who asked; null when the account is gone. | | `data[].reviewer_name` | string \| null | yes | That person's name, when the reviewer was loaded; null otherwise. | | `data[].reviewed_at` | string | yes | When Heimdall answered, ISO 8601. | | `data[].created_at` | string \| null | yes | When the review was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `business-rule:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the business rule is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List a business rule's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-versions `GET /v1/business-rules/{rule}/revisions` ```bash curl "https://api.example.test/v1/business-rules/$RULE_ID/revisions" \ -H "Authorization: Bearer $TOKEN" ``` Every version of the rule, newest first, 25 per page (`per_page` 1 to 100). Each carries its `version` number, the `reason` it was written, who wrote it (`author_id`, `author_name`) and through what (`actor_kind`), a one-line `summary`, the `changed_fields`, the `diff` from the version before and the full `snapshot`, the examples included. A client guest's token is refused. - Requires ability: `business-rule:view` - MCP tools: `get_business_rule` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The rule, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role does not carry `business-rule:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List business rules Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-business-rules `GET /v1/projects/{project}/business-rules` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/business-rules?type=computation&status=approved" \ -H "Authorization: Bearer $TOKEN" ``` The rules of one project in the project's order, 25 per page (`per_page` 1 to 100), each with its owner's and its authority stakeholder's names. `sort_by` takes `number`, `reference`, `name`, `type`, `status`, `priority`, `effective_from`, `effective_to`, `position`, `created_at` or `updated_at`. `type`, `status`, `priority` and `owner_id` narrow the page to one value each, and `search` to the rules whose reference, name or authority contain the words. - Requires ability: `business-rule:view-any` - MCP tools: `list_business_rules` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `type` | query | `constraint`, `derivation`, `computation`, `inference`, `action_enabler`, `policy` \| null | no | Narrow to one kind: `constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`. | | `status` | query | `draft`, `in_review`, `approved`, `baselined`, `deprecated` \| null | no | Narrow to one status: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. An unknown status is refused. | | `priority` | query | `must`, `should`, `could`, `wont` \| null | no | Narrow to one MoSCoW priority: `must`, `should`, `could` or `wont`. | | `owner_id` | query | string \| null (uuid) | no | Narrow to the rules one person answers for. | | `search` | query | string \| null | no | Words the reference, the name or the authority must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of rules. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The rule's id. | | `data[].project_id` | string | yes | The project the rule belongs to. | | `data[].number` | integer | yes | Its number in the rules' own sequence. | | `data[].reference` | string | yes | The key requirements, stories, test cases and people use, such as `CRM-BR-7`. | | `data[].name` | string | yes | What the rule is called. | | `data[].type` | string | yes | The kind: `constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`. | | `data[].statement` | string \| null | yes | The rule in plain words, as HTML filtered to a safe subset; null when none. | | `data[].condition` | string \| null | yes | When the rule applies, in one line; null when none. | | `data[].action` | string \| null | yes | What follows when it does, in one line; null when none. | | `data[].exceptions` | string \| null | yes | When the rule does not apply, as HTML; null when none. | | `data[].examples` | array of object | yes | Worked examples in order, each `{ "input": …, "expected": … }`; empty when none. | | `data[].examples[].input` | string | yes | The input of a worked example. | | `data[].examples[].expected` | string | yes | What the rule gives for that input; empty when not written. | | `data[].authority_stakeholder_id` | string \| null | yes | The stakeholder who may change the rule; null when none. | | `data[].authority_stakeholder_name` | string \| null | yes | That stakeholder's name, when the authority was loaded; null otherwise. | | `data[].authority` | string \| null | yes | Who or what the rule's authority is, in words; null when none. | | `data[].priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data[].effective_from` | string \| null | yes | When the rule comes into force, `Y-m-d`; null when always. | | `data[].effective_to` | string \| null | yes | When it stops applying, `Y-m-d`; null when open-ended. | | `data[].status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data[].owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data[].owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data[].position` | integer | yes | Its place among the project's rules, from zero. | | `data[].version` | integer | yes | How many times it was approved; zero until the first approval. | | `data[].approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data[].approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data[].created_at` | string \| null | yes | When the rule was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the rule last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `type`, `status` or `priority` is not one of its values, `owner_id` is not a uuid, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a business rule Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/move-a-business-rule `POST /v1/business-rules/{rule}/status` ```bash curl -X POST https://api.example.test/v1/business-rules/$RULE_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "in_review"}' ``` Moves the rule along its lifecycle. A draft goes to `in_review` or `deprecated`; a review goes back to `draft`, on to `approved` or to `deprecated`; an approved rule goes back to `in_review`, on to `baselined` or to `deprecated`; a baselined one is reopened to `in_review` or goes to `deprecated`; a deprecated one is revived as a `draft`. A move into `approved` stamps when the rule was agreed, and a client guest reads it from that moment on, through baselined, and loses it when it is deprecated. The move writes a version with the reason `transition`. Answers `200` with the rule and emits `business_rule.status_changed` with the status before and after; a move to the status the rule is in already answers `200` and emits nothing. - Requires ability: `business-rule:update` - Fires events: `business_rule.status_changed` - MCP tools: `update_business_rule` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The rule, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the business rule to. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | yes | Where the rule goes: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. The move must be one the lifecycle allows from the current status. | ### Responses - **200**: The rule, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The rule's id. | | `data.project_id` | string | yes | The project the rule belongs to. | | `data.number` | integer | yes | Its number in the rules' own sequence. | | `data.reference` | string | yes | The key requirements, stories, test cases and people use, such as `CRM-BR-7`. | | `data.name` | string | yes | What the rule is called. | | `data.type` | string | yes | The kind: `constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`. | | `data.statement` | string \| null | yes | The rule in plain words, as HTML filtered to a safe subset; null when none. | | `data.condition` | string \| null | yes | When the rule applies, in one line; null when none. | | `data.action` | string \| null | yes | What follows when it does, in one line; null when none. | | `data.exceptions` | string \| null | yes | When the rule does not apply, as HTML; null when none. | | `data.examples` | array of object | yes | Worked examples in order, each `{ "input": …, "expected": … }`; empty when none. | | `data.examples[].input` | string | yes | The input of a worked example. | | `data.examples[].expected` | string | yes | What the rule gives for that input; empty when not written. | | `data.authority_stakeholder_id` | string \| null | yes | The stakeholder who may change the rule; null when none. | | `data.authority_stakeholder_name` | string \| null | yes | That stakeholder's name, when the authority was loaded; null otherwise. | | `data.authority` | string \| null | yes | Who or what the rule's authority is, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.effective_from` | string \| null | yes | When the rule comes into force, `Y-m-d`; null when always. | | `data.effective_to` | string \| null | yes | When it stops applying, `Y-m-d`; null when open-ended. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's rules, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the rule was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the rule last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `business-rule:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, or when the lifecycle refuses the move. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a business rule Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/restore-a-version-of-a-business-rule `POST /v1/business-rules/{rule}/revisions/{revision}/restore` ```bash curl -X POST https://api.example.test/v1/business-rules/$RULE_ID/revisions/$REVISION_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the version's captured columns back onto the rule, status and examples included, and saves. The save writes a new version with the reason `restore` that names the one put back in `restored_from_id`; nothing earlier is rewritten. Answers `201` with the version written and emits `business_rule.restored` with both version numbers. - Requires ability: `business-rule:update` - Fires events: `business_rule.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The rule, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version the restore wrote, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `business-rule:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the version is not the rule's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the version is identical to the current record, so there is nothing to restore. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Review a business rule's quality Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/review-a-business-rules-quality `POST /v1/business-rules/{rule}/quality-reviews` ```bash curl -X POST https://api.example.test/v1/business-rules/{rule}/quality-reviews \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Asks Heimdall to read the business rule for its condition, action, exceptions, worked examples and authority, and for conflicts and overlaps with the project's other rules. The review is kept with its findings, the score they leave (100 less 25 per `high`, 10 per `medium` and 4 per `low` finding, never below 0), its band (`ready` at 80 and above, `needs_work` from 50, `not_ready` below) and the version of the business rule it read; the call is filed in the AI ledger under the team's monthly budget. Nothing on the business rule changes. Answers `201` with the review and emits `business_rule.quality_reviewed`. Read the latest review first: every call spends. - Requires ability: `ai:use` - Fires events: `business_rule.quality_reviewed` - MCP tools: `review_quality` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The business rule, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The review, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The review's id. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.artefact_type` | string | yes | The artefact's type: `work-item`, `requirement` or `business-rule`. | | `data.artefact_id` | string | yes | The artefact's id. | | `data.version` | integer \| null | yes | The version of the artefact Heimdall read, as its history numbers them; null when the artefact had no version yet. | | `data.score` | integer | yes | 0 to 100: 100 less each finding's penalty (25 for `high`, 10 for `medium`, 4 for `low`), never below 0. | | `data.band` | string | yes | What the score means: `ready` at 80 and above, `needs_work` from 50 to 79, `not_ready` below 50. | | `data.summary` | string \| null | yes | Two or three sentences on the record as a whole; null when the model gave none. | | `data.findings` | array of object | yes | The findings, the most serious first; empty when the record was found sound. | | `data.findings[].code` | string | yes | The kind of problem: `independent`, `negotiable`, `valuable`, `estimable`, `small` or `testable` for an INVEST breach, `ambiguous`, `missing_criteria`, `untestable`, `missing_measure`, `missing_rationale`, `missing_examples`, `incomplete`, `rule_conflict`, `duplicate`, `stack_drift` or `other`. | | `data.findings[].severity` | string | yes | How much it matters: `high` (a developer or a tester would be blocked or misled), `medium` (usable but would be argued over) or `low` (polish). | | `data.findings[].message` | string | yes | What is wrong or missing, quoting the words concerned. | | `data.findings[].suggestion` | string \| null | yes | What to write instead, in the record's own words; null when there is nothing to suggest. | | `data.findings[].field` | string \| null | yes | The field concerned, such as `title`, `description`, `criteria`, `statement` or `measure`; null for the whole record. | | `data.findings_count` | integer | yes | How many findings the review carries. | | `data.provider` | string | yes | The provider that answered, by its configuration key. | | `data.model` | string | yes | The model that answered. | | `data.usage_id` | string \| null | yes | The ledger row that filed the call's cost; null when that row is gone. | | `data.reviewed_by` | string \| null | yes | The person who asked; null when the account is gone. | | `data.reviewer_name` | string \| null | yes | That person's name, when the reviewer was loaded; null otherwise. | | `data.reviewed_at` | string | yes | When Heimdall answered, ISO 8601. | | `data.created_at` | string \| null | yes | When the review was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token does not carry `ai:use`, is a client guest's, or the project's assists are off. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the business rule is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `AI_BUDGET_EXHAUSTED`: when the team's monthly AI budget has no room; nothing is filed. `VALIDATION_FAILED`: when Heimdall could not reach its model; nothing is filed or written, and a retry a moment later is safe. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a business rule Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/update-a-business-rule `PATCH /v1/business-rules/{rule}` ```bash curl -X PATCH https://api.example.test/v1/business-rules/$RULE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"priority": "must", "effective_from": "2027-01-01", "authority": null}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. `examples`, when sent, replaces the list in full. The status does not change here; the move endpoint owns it. A save that changes a captured column writes a new version. Answers `200` with the rule and emits `business_rule.updated` with the columns that changed, each with its old and new value; an update that changes no column answers `200` and emits nothing. - Requires ability: `business-rule:update` - Fires events: `business_rule.updated` - MCP tools: `update_business_rule` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `rule` | path | string (uuid) | yes | The rule, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A business rule to add or change. Only `name` is required on a create; the kind defaults to `constraint`, the priority to `should` and the status to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; `examples`, when sent, replaces the list; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | What the rule is called, 3 to 200 characters. | | `type` | `constraint`, `derivation`, `computation`, `inference`, `action_enabler`, `policy` | no | The kind: `constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`. Defaults to `constraint`. | | `statement` | string \| null | no | The rule in plain words, as HTML; filtered to a safe subset when read. | | `condition` | string \| null | no | When the rule applies, in one line up to 500 characters. | | `action` | string \| null | no | What follows when it does, in one line up to 500 characters. | | `exceptions` | string \| null | no | When the rule does not apply, as HTML. | | `examples` | array \| null | no | Worked examples, up to twenty rows of `input` (required) and `expected`. Sent, the list is replaced; left out, it stays as it is. | | `examples[].input` | string | yes | The input of a worked example, up to 500 characters; required. | | `examples[].expected` | string \| null | no | What the rule gives for that input, up to 500 characters. | | `authority_stakeholder_id` | string \| null (uuid) | no | The id of the stakeholder who may change the rule, a stakeholder of the same project; null for none. | | `authority` | string \| null | no | Who or what the rule's authority is, in words, up to 300 characters. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority: `must`, `should`, `could` or `wont`. Defaults to `should`. | | `effective_from` | string \| null (date) | no | When the rule comes into force, `Y-m-d`. | | `effective_to` | string \| null (date) | no | When it stops applying, `Y-m-d`; on or after the start. | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | no | Where it starts, on a create only: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. Defaults to `draft`. | | `owner_id` | string \| null (uuid) | no | The id of the person who answers for it, a member of the team; null for nobody. | ### Responses - **200**: The rule. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The rule's id. | | `data.project_id` | string | yes | The project the rule belongs to. | | `data.number` | integer | yes | Its number in the rules' own sequence. | | `data.reference` | string | yes | The key requirements, stories, test cases and people use, such as `CRM-BR-7`. | | `data.name` | string | yes | What the rule is called. | | `data.type` | string | yes | The kind: `constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`. | | `data.statement` | string \| null | yes | The rule in plain words, as HTML filtered to a safe subset; null when none. | | `data.condition` | string \| null | yes | When the rule applies, in one line; null when none. | | `data.action` | string \| null | yes | What follows when it does, in one line; null when none. | | `data.exceptions` | string \| null | yes | When the rule does not apply, as HTML; null when none. | | `data.examples` | array of object | yes | Worked examples in order, each `{ "input": …, "expected": … }`; empty when none. | | `data.examples[].input` | string | yes | The input of a worked example. | | `data.examples[].expected` | string | yes | What the rule gives for that input; empty when not written. | | `data.authority_stakeholder_id` | string \| null | yes | The stakeholder who may change the rule; null when none. | | `data.authority_stakeholder_name` | string \| null | yes | That stakeholder's name, when the authority was loaded; null otherwise. | | `data.authority` | string \| null | yes | Who or what the rule's authority is, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.effective_from` | string \| null | yes | When the rule comes into force, `Y-m-d`; null when always. | | `data.effective_to` | string \| null | yes | When it stops applying, `Y-m-d`; null when open-ended. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's rules, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the rule was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the rule last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `business-rule:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when the dates in force fall out of order, when the authority is not a stakeholder of the project, or when the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Analyse a change request's impact Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/analyse-a-change-requests-impact `POST /v1/change-requests/{changeRequest}/analyse` ```bash curl -X POST https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID/analyse \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Asks Heimdall to read the request against its baseline and the records it names, each with what links to it. The reading is written as the request's draft (`ai_impact_draft`: the summary, the scope, the risk, the records touched and the questions to ask before deciding) and fills the impact fields the analyst left empty (the level, the schedule, the effort, the scope and the risk); a number the analyst already wrote is never replaced. A request still only submitted moves under analysis. The call is metered against the team's budget and filed under the impact analysis feature; the save writes a version with the reason `ai_assist`. Answers `200` with the request and emits `change_request.analysed`. - Requires ability: `change-request:analyse` - Fires events: `change_request.analysed` - MCP tools: `analyse_change_impact` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The request, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The request with Heimdall's draft on it. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The request's id. | | `data.project_id` | string | yes | The project the request belongs to. | | `data.number` | integer | yes | Its number in the change requests' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-CR-2`. | | `data.title` | string | yes | What the change is called. | | `data.description` | string \| null | yes | What is asked for and why, as HTML filtered to a safe subset; null when none. | | `data.reason` | string | yes | Why it was raised: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. | | `data.requested_by` | string \| null | yes | The person who raised it; null when that account is gone. | | `data.requester_name` | string \| null | yes | Their name, when it was loaded; null otherwise. | | `data.stakeholder_id` | string \| null | yes | The stakeholder who asked, when the client did; that seat co-signs the impact. Null otherwise. | | `data.stakeholder_name` | string \| null | yes | That stakeholder's name, when it was loaded; null otherwise. | | `data.baseline_id` | string \| null | yes | The baseline the change is raised against; null when none. | | `data.baseline_name` | string \| null | yes | That baseline's name, when it was loaded; null otherwise. | | `data.affected` | array of object | no | The records the request touches, when they were loaded. | | `data.affected[].artefact_type` | string | yes | The record's family. | | `data.affected[].artefact_id` | string | yes | The record's id. | | `data.affected[].reference` | string \| null | yes | The record's key; null when the record is gone or hidden. | | `data.affected[].title` | string \| null | yes | The record's title; null when the record is gone or hidden. | | `data.impact_scope` | string \| null | yes | What the change touches, as HTML; null until analysed. | | `data.impact_schedule_days` | integer \| null | yes | Days the schedule moves; negative when it shortens. Null until analysed. | | `data.impact_effort_hours` | string \| null | no | Hours of effort, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_cost` | string \| null | no | The cost, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_risk` | string \| null | yes | What the change risks, as HTML; null until analysed. | | `data.impact_level` | string \| null | yes | How hard it hits the project: `low`, `medium` or `high`; null until analysed. | | `data.ai_impact_draft` | string \| null | no | Heimdall's reading, as HTML; null until asked for. Left out for a guest. | | `data.analysed_at` | string \| null | yes | When Heimdall last read it, ISO 8601; null when never. | | `data.priority` | string | yes | How urgent: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.status` | string | yes | Where it stands: `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified` or `closed`. | | `data.decision_note` | string \| null | yes | The approvers' words when it was refused; null otherwise. | | `data.verified_by` | string \| null | yes | The person who confirmed the change landed; null until verified. | | `data.verified_at` | string \| null | yes | When it was verified, ISO 8601; null until then. | | `data.version` | integer | yes | Its approved version: 0 while never approved. | | `data.approved_at` | string \| null | yes | When the approval carried, ISO 8601; null until then. | | `data.approved_revision_id` | string \| null | yes | The revision the approval froze; null until then. | | `data.position` | integer | yes | Its place among the project's change requests, from zero. | | `data.created_at` | string \| null | yes | When the request was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the request last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, the project's assists are off, or its holder's role does not carry `change-request:analyse` and `ai:use`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `AI_BUDGET_EXHAUSTED`: when the team's month has no room left. `VALIDATION_FAILED`: when the provider did not answer. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a change request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/delete-a-change-request `DELETE /v1/change-requests/{changeRequest}` ```bash curl -X DELETE https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a change request that was never agreed (submitted, under analysis or rejected) with its links, its versions and its activity history. A request past its gate is closed rather than deleted. The number it took is never handed out again. Returns `204 No Content` and emits `change_request.deleted`. - Requires ability: `change-request:delete` - Fires events: `change_request.deleted` - MCP tools: `delete_change_request` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The request, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `change-request:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the request was agreed or is past the gate. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a change request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-change-request `GET /v1/change-requests/{changeRequest}` ```bash curl https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID \ -H "Authorization: Bearer $TOKEN" ``` One change request with the records it touches. A request of a project the token may not see, or one a client guest neither raised nor asked for, is `404 RESOURCE_NOT_FOUND`. - `reason` is `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`; `status` is `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified` or `closed`; `impact_level` is `low`, `medium` or `high`; `priority` is `lowest`, `low`, `medium`, `high` or `highest`. - Requires ability: `change-request:view` - MCP tools: `get_change_request` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The request, bound by the middleware. | ### Responses - **200**: The request with the records it touches. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The request's id. | | `data.project_id` | string | yes | The project the request belongs to. | | `data.number` | integer | yes | Its number in the change requests' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-CR-2`. | | `data.title` | string | yes | What the change is called. | | `data.description` | string \| null | yes | What is asked for and why, as HTML filtered to a safe subset; null when none. | | `data.reason` | string | yes | Why it was raised: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. | | `data.requested_by` | string \| null | yes | The person who raised it; null when that account is gone. | | `data.requester_name` | string \| null | yes | Their name, when it was loaded; null otherwise. | | `data.stakeholder_id` | string \| null | yes | The stakeholder who asked, when the client did; that seat co-signs the impact. Null otherwise. | | `data.stakeholder_name` | string \| null | yes | That stakeholder's name, when it was loaded; null otherwise. | | `data.baseline_id` | string \| null | yes | The baseline the change is raised against; null when none. | | `data.baseline_name` | string \| null | yes | That baseline's name, when it was loaded; null otherwise. | | `data.affected` | array of object | no | The records the request touches, when they were loaded. | | `data.affected[].artefact_type` | string | yes | The record's family. | | `data.affected[].artefact_id` | string | yes | The record's id. | | `data.affected[].reference` | string \| null | yes | The record's key; null when the record is gone or hidden. | | `data.affected[].title` | string \| null | yes | The record's title; null when the record is gone or hidden. | | `data.impact_scope` | string \| null | yes | What the change touches, as HTML; null until analysed. | | `data.impact_schedule_days` | integer \| null | yes | Days the schedule moves; negative when it shortens. Null until analysed. | | `data.impact_effort_hours` | string \| null | no | Hours of effort, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_cost` | string \| null | no | The cost, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_risk` | string \| null | yes | What the change risks, as HTML; null until analysed. | | `data.impact_level` | string \| null | yes | How hard it hits the project: `low`, `medium` or `high`; null until analysed. | | `data.ai_impact_draft` | string \| null | no | Heimdall's reading, as HTML; null until asked for. Left out for a guest. | | `data.analysed_at` | string \| null | yes | When Heimdall last read it, ISO 8601; null when never. | | `data.priority` | string | yes | How urgent: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.status` | string | yes | Where it stands: `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified` or `closed`. | | `data.decision_note` | string \| null | yes | The approvers' words when it was refused; null otherwise. | | `data.verified_by` | string \| null | yes | The person who confirmed the change landed; null until verified. | | `data.verified_at` | string \| null | yes | When it was verified, ISO 8601; null until then. | | `data.version` | integer | yes | Its approved version: 0 while never approved. | | `data.approved_at` | string \| null | yes | When the approval carried, ISO 8601; null until then. | | `data.approved_revision_id` | string \| null | yes | The revision the approval froze; null until then. | | `data.position` | integer | yes | Its place among the project's change requests, from zero. | | `data.created_at` | string \| null | yes | When the request was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the request last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a version of a change request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-version-of-a-change-request `GET /v1/change-requests/{changeRequest}/revisions/{revision}` ```bash curl https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID/revisions/$REVISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One version of the request. A version of another artefact or a version the token may not see is `404 RESOURCE_NOT_FOUND`. - Requires ability: `change-request:view` - MCP tools: `get_change_request` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The request, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Change Requests API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests A **change request** is a change asked for after the record was agreed: a new requirement, a clarification, a defect in the baseline, a regulatory demand, a client's wish, a piece of technical debt or a scope reduction. It numbers on its own sequence (`CRM-CR-2`), names who asked, the baseline it changes and, as links from the request, the records it touches. An analyst writes the impact on scope, schedule, effort, cost and risk, or asks Heimdall for a draft, and the approvers read the level first. Its approval is the one door through which a frozen baseline's items are unlocked; a rejection keeps the approver's words as the request's decision note. Every save of a change request writes a **version**; the versions are read and put back through the endpoints below, under the change requests' own abilities. A staff token sees the change requests of the projects it may see. A client guest's token raises requests and sees the ones they raised or asked for through their stakeholder seat, without the effort, the cost and Heimdall's draft, which are the team's working numbers until the approvers decide; any other request it asks for is a `404 RESOURCE_NOT_FOUND`. Every write runs the same action the Change Requests page runs, under the project permissions, and announces itself as a `change_request.*` event. Putting a request to its approvers is on the [approvals endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals). ## Endpoints - `GET /v1/change-requests/{changeRequest}` — [Get a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-change-request) - `PATCH /v1/change-requests/{changeRequest}` — [Update a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/update-a-change-request) - `DELETE /v1/change-requests/{changeRequest}` — [Delete a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/delete-a-change-request) - `GET /v1/change-requests/{changeRequest}/revisions` — [List a change request's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-a-change-requests-versions) - `GET /v1/change-requests/{changeRequest}/revisions/{revision}` — [Get a version of a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-version-of-a-change-request) - `GET /v1/projects/{project}/change-requests` — [List change requests](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-change-requests) - `POST /v1/projects/{project}/change-requests` — [Raise a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/raise-a-change-request) - `POST /v1/change-requests/{changeRequest}/status` — [Move a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/move-a-change-request) - `POST /v1/change-requests/{changeRequest}/verify` — [Verify a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/verify-a-change-request) - `POST /v1/change-requests/{changeRequest}/analyse` — [Analyse a change request's impact](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/analyse-a-change-requests-impact) - `POST /v1/change-requests/{changeRequest}/revisions/{revision}/restore` — [Restore a version of a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/restore-a-version-of-a-change-request) ## Related - [Baselines API](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines): The frozen records a change request unlocks. - [Links API](https://alm-docs.labs.zuselab.dev/api/v1/reference/links): The records a request touches are links from it, with the kind relates_to. - [Approvals API](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals): The gate a change request passes. - [change_request.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request): The life of a project's change requests: a request raised, its words or impact changed, moved along its lifecycle by hand or by the approval engine, read by Heimdall for its impact, put back to an earlier version and deleted. --- # List a change request's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-a-change-requests-versions `GET /v1/change-requests/{changeRequest}/revisions` ```bash curl "https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID/revisions" \ -H "Authorization: Bearer $TOKEN" ``` Every version of the request, newest first, 25 per page (`per_page` 1 to 100). Each carries its `version` number, the `reason` it was written (`ai_assist` for Heimdall's draft), who wrote it (`author_id`, `author_name`) and through what (`actor_kind`), a one-line `summary`, the `changed_fields`, the `diff` from the version before and the full `snapshot`. - Requires ability: `change-request:view` - MCP tools: `get_change_request` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The request, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role does not carry `change-request:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List change requests Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-change-requests `GET /v1/projects/{project}/change-requests` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/change-requests?status=awaiting_approval" \ -H "Authorization: Bearer $TOKEN" ``` The change requests of one project in the project's order, 25 per page (`per_page` 1 to 100), each with the names of its baseline, its requester and its stakeholder. `sort_by` takes `number`, `reference`, `title`, `reason`, `priority`, `impact_level`, `status`, `position`, `created_at` or `updated_at`. `reason`, `status`, `priority`, `impact_level` and `baseline_id` narrow the page to one value each, and `search` to the requests whose reference or title contain the words. A client guest's token receives the requests they raised or asked for. - Requires ability: `change-request:view-any` - MCP tools: `list_change_requests` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `reason` | query | `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt`, `scope_reduction` \| null | no | Narrow to one reason: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. | | `status` | query | `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified`, `closed` \| null | no | Narrow to one status: `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified` or `closed`. | | `priority` | query | `lowest`, `low`, `medium`, `high`, `highest` \| null | no | Narrow to one priority: `lowest`, `low`, `medium`, `high` or `highest`. | | `impact_level` | query | `low`, `medium`, `high` \| null | no | Narrow to one impact level: `low`, `medium` or `high`. | | `baseline_id` | query | string \| null (uuid) | no | Narrow to the requests raised against one baseline. | | `search` | query | string \| null | no | Words the reference or the title must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of change requests. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The request's id. | | `data[].project_id` | string | yes | The project the request belongs to. | | `data[].number` | integer | yes | Its number in the change requests' own sequence. | | `data[].reference` | string | yes | The key people use, such as `CRM-CR-2`. | | `data[].title` | string | yes | What the change is called. | | `data[].description` | string \| null | yes | What is asked for and why, as HTML filtered to a safe subset; null when none. | | `data[].reason` | string | yes | Why it was raised: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. | | `data[].requested_by` | string \| null | yes | The person who raised it; null when that account is gone. | | `data[].requester_name` | string \| null | yes | Their name, when it was loaded; null otherwise. | | `data[].stakeholder_id` | string \| null | yes | The stakeholder who asked, when the client did; that seat co-signs the impact. Null otherwise. | | `data[].stakeholder_name` | string \| null | yes | That stakeholder's name, when it was loaded; null otherwise. | | `data[].baseline_id` | string \| null | yes | The baseline the change is raised against; null when none. | | `data[].baseline_name` | string \| null | yes | That baseline's name, when it was loaded; null otherwise. | | `data[].affected` | array of object | no | The records the request touches, when they were loaded. | | `data[].affected[].artefact_type` | string | yes | The record's family. | | `data[].affected[].artefact_id` | string | yes | The record's id. | | `data[].affected[].reference` | string \| null | yes | The record's key; null when the record is gone or hidden. | | `data[].affected[].title` | string \| null | yes | The record's title; null when the record is gone or hidden. | | `data[].impact_scope` | string \| null | yes | What the change touches, as HTML; null until analysed. | | `data[].impact_schedule_days` | integer \| null | yes | Days the schedule moves; negative when it shortens. Null until analysed. | | `data[].impact_effort_hours` | string \| null | no | Hours of effort, as a decimal string. Null until analysed; left out for a guest. | | `data[].impact_cost` | string \| null | no | The cost, as a decimal string. Null until analysed; left out for a guest. | | `data[].impact_risk` | string \| null | yes | What the change risks, as HTML; null until analysed. | | `data[].impact_level` | string \| null | yes | How hard it hits the project: `low`, `medium` or `high`; null until analysed. | | `data[].ai_impact_draft` | string \| null | no | Heimdall's reading, as HTML; null until asked for. Left out for a guest. | | `data[].analysed_at` | string \| null | yes | When Heimdall last read it, ISO 8601; null when never. | | `data[].priority` | string | yes | How urgent: `lowest`, `low`, `medium`, `high` or `highest`. | | `data[].status` | string | yes | Where it stands: `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified` or `closed`. | | `data[].decision_note` | string \| null | yes | The approvers' words when it was refused; null otherwise. | | `data[].verified_by` | string \| null | yes | The person who confirmed the change landed; null until verified. | | `data[].verified_at` | string \| null | yes | When it was verified, ISO 8601; null until then. | | `data[].version` | integer | yes | Its approved version: 0 while never approved. | | `data[].approved_at` | string \| null | yes | When the approval carried, ISO 8601; null until then. | | `data[].approved_revision_id` | string \| null | yes | The revision the approval froze; null until then. | | `data[].position` | integer | yes | Its place among the project's change requests, from zero. | | `data[].created_at` | string \| null | yes | When the request was raised, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the request last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `change-request:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a filter is not one of its values, `baseline_id` is not a uuid, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a change request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/move-a-change-request `POST /v1/change-requests/{changeRequest}/status` ```bash curl -X POST https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "implementing"}' ``` Moves a change request by hand: `under_analysis` from submitted (under `change-request:analyse`), `implementing` from approved or implemented, `implemented` from implementing, and `closed` from submitted, under analysis, approved, rejected or verified. The gate's states (`awaiting_approval`, `approved`, `rejected`) are the approval engine's and are refused here; `verified` has its own endpoint under its own ability. The move writes a version with the reason `transition`. Answers `200` with the request and emits `change_request.status_changed` with the status before and after; a move to the status the request is in already answers `200` and emits nothing. - Requires ability: `change-request:update` - Fires events: `change_request.status_changed` - MCP tools: `update_change_request` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The request, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the change request to by hand: into analysis, into the work and out of it. The gate's states (`awaiting_approval`, `approved`, `rejected`) are the approval engine's and are refused here; `verified` has its own endpoint under its own ability. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified`, `closed` | yes | Where the request goes: `under_analysis` from submitted; `implementing` from approved or implemented; `implemented` from implementing; `closed` from submitted, under analysis, approved, rejected or verified. A move the lifecycle or the engine refuses answers 422. | ### Responses - **200**: The request, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The request's id. | | `data.project_id` | string | yes | The project the request belongs to. | | `data.number` | integer | yes | Its number in the change requests' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-CR-2`. | | `data.title` | string | yes | What the change is called. | | `data.description` | string \| null | yes | What is asked for and why, as HTML filtered to a safe subset; null when none. | | `data.reason` | string | yes | Why it was raised: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. | | `data.requested_by` | string \| null | yes | The person who raised it; null when that account is gone. | | `data.requester_name` | string \| null | yes | Their name, when it was loaded; null otherwise. | | `data.stakeholder_id` | string \| null | yes | The stakeholder who asked, when the client did; that seat co-signs the impact. Null otherwise. | | `data.stakeholder_name` | string \| null | yes | That stakeholder's name, when it was loaded; null otherwise. | | `data.baseline_id` | string \| null | yes | The baseline the change is raised against; null when none. | | `data.baseline_name` | string \| null | yes | That baseline's name, when it was loaded; null otherwise. | | `data.affected` | array of object | no | The records the request touches, when they were loaded. | | `data.affected[].artefact_type` | string | yes | The record's family. | | `data.affected[].artefact_id` | string | yes | The record's id. | | `data.affected[].reference` | string \| null | yes | The record's key; null when the record is gone or hidden. | | `data.affected[].title` | string \| null | yes | The record's title; null when the record is gone or hidden. | | `data.impact_scope` | string \| null | yes | What the change touches, as HTML; null until analysed. | | `data.impact_schedule_days` | integer \| null | yes | Days the schedule moves; negative when it shortens. Null until analysed. | | `data.impact_effort_hours` | string \| null | no | Hours of effort, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_cost` | string \| null | no | The cost, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_risk` | string \| null | yes | What the change risks, as HTML; null until analysed. | | `data.impact_level` | string \| null | yes | How hard it hits the project: `low`, `medium` or `high`; null until analysed. | | `data.ai_impact_draft` | string \| null | no | Heimdall's reading, as HTML; null until asked for. Left out for a guest. | | `data.analysed_at` | string \| null | yes | When Heimdall last read it, ISO 8601; null when never. | | `data.priority` | string | yes | How urgent: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.status` | string | yes | Where it stands: `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified` or `closed`. | | `data.decision_note` | string \| null | yes | The approvers' words when it was refused; null otherwise. | | `data.verified_by` | string \| null | yes | The person who confirmed the change landed; null until verified. | | `data.verified_at` | string \| null | yes | When it was verified, ISO 8601; null until then. | | `data.version` | integer | yes | Its approved version: 0 while never approved. | | `data.approved_at` | string \| null | yes | When the approval carried, ISO 8601; null until then. | | `data.approved_revision_id` | string \| null | yes | The revision the approval froze; null until then. | | `data.position` | integer | yes | Its place among the project's change requests, from zero. | | `data.created_at` | string \| null | yes | When the request was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the request last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry the permission the move needs: `change-request:update`, or `change-request:analyse` into analysis. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, the lifecycle refuses the move, or the move is the approval engine's. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Raise a change request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/raise-a-change-request `POST /v1/projects/{project}/change-requests` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/change-requests \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "title": "Accept vouchers on subscriptions too", "reason": "client_request", "description": "

The client wants the voucher applied to the first renewal as well.

", "baseline_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "stakeholder_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b", "affected": [{"type": "requirement", "id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13"}], "priority": "high" }' ``` Only `title` is required. The request takes the next number of the change requests' sequence and the reference spelled from the project's key, starts `submitted`, as a `new_requirement` unless `reason` says otherwise and at `medium` priority unless `priority` does, and is raised by the token's holder. The baseline and the stakeholder must be the project's; the records under `affected` are linked from the request with the kind `relates_to` and must be the project's own. A client guest's token raises the request without the impact fields, with their own seat as the stakeholder when an account is linked to it. The save writes version one of the request. Answers `201` with the request and emits `change_request.created`. - Requires ability: `change-request:create` - Fires events: `change_request.created` - MCP tools: `raise_change_request` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A change request to raise or change. Only `title` is required on a create; the reason defaults to `new_requirement` and the priority to `medium`. On an update, a field left out keeps its value and a field sent as null is cleared; `affected`, when sent, replaces the records the request touches; the status moves through its own endpoints. A guest's token may raise a request but not write its impact. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | yes | What the change is called, 3 to 200 characters. | | `description` | string \| null | no | What is asked for and why, as HTML; filtered to a safe subset when read. | | `reason` | `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt`, `scope_reduction` | no | Why it was raised: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. Defaults to `new_requirement`. | | `stakeholder_id` | string \| null (uuid) | no | The stakeholder who asked, a stakeholder of the project; that seat co-signs the impact when an account is linked to it. Null for none. | | `baseline_id` | string \| null (uuid) | no | The baseline the change is raised against, a baseline of the project; its items among the records touched are unlocked when the request is approved. Null for none. | | `affected` | array \| null | no | The records the request touches, up to 200. Sent, they replace the list; left out, the list stays. | | `affected[].type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The record's family: `epic`, `work-item`, `requirement`, `business-rule` or `use-case`. | | `affected[].id` | string (uuid) | yes | The record's id. | | `impact_scope` | string \| null | no | What the change touches, as HTML. Ignored for a guest. | | `impact_schedule_days` | integer \| null | no | Days the schedule moves, -3650 to 3650; negative when it shortens. Ignored for a guest. | | `impact_effort_hours` | number \| null | no | Hours of effort, 0 to 999,999.99. Ignored for a guest. | | `impact_cost` | number \| null | no | The cost in the project's currency, 0 to 9,999,999,999.99. Ignored for a guest. | | `impact_risk` | string \| null | no | What the change risks, as HTML. Ignored for a guest. | | `impact_level` | `low`, `medium`, `high` \| null | no | How hard it hits the project: `low`, `medium` or `high`; null until judged. Ignored for a guest. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | How urgent: `lowest`, `low`, `medium`, `high` or `highest`. Defaults to `medium`. | ### Responses - **201**: The request, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The request's id. | | `data.project_id` | string | yes | The project the request belongs to. | | `data.number` | integer | yes | Its number in the change requests' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-CR-2`. | | `data.title` | string | yes | What the change is called. | | `data.description` | string \| null | yes | What is asked for and why, as HTML filtered to a safe subset; null when none. | | `data.reason` | string | yes | Why it was raised: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. | | `data.requested_by` | string \| null | yes | The person who raised it; null when that account is gone. | | `data.requester_name` | string \| null | yes | Their name, when it was loaded; null otherwise. | | `data.stakeholder_id` | string \| null | yes | The stakeholder who asked, when the client did; that seat co-signs the impact. Null otherwise. | | `data.stakeholder_name` | string \| null | yes | That stakeholder's name, when it was loaded; null otherwise. | | `data.baseline_id` | string \| null | yes | The baseline the change is raised against; null when none. | | `data.baseline_name` | string \| null | yes | That baseline's name, when it was loaded; null otherwise. | | `data.affected` | array of object | no | The records the request touches, when they were loaded. | | `data.affected[].artefact_type` | string | yes | The record's family. | | `data.affected[].artefact_id` | string | yes | The record's id. | | `data.affected[].reference` | string \| null | yes | The record's key; null when the record is gone or hidden. | | `data.affected[].title` | string \| null | yes | The record's title; null when the record is gone or hidden. | | `data.impact_scope` | string \| null | yes | What the change touches, as HTML; null until analysed. | | `data.impact_schedule_days` | integer \| null | yes | Days the schedule moves; negative when it shortens. Null until analysed. | | `data.impact_effort_hours` | string \| null | no | Hours of effort, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_cost` | string \| null | no | The cost, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_risk` | string \| null | yes | What the change risks, as HTML; null until analysed. | | `data.impact_level` | string \| null | yes | How hard it hits the project: `low`, `medium` or `high`; null until analysed. | | `data.ai_impact_draft` | string \| null | no | Heimdall's reading, as HTML; null until asked for. Left out for a guest. | | `data.analysed_at` | string \| null | yes | When Heimdall last read it, ISO 8601; null when never. | | `data.priority` | string | yes | How urgent: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.status` | string | yes | Where it stands: `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified` or `closed`. | | `data.decision_note` | string \| null | yes | The approvers' words when it was refused; null otherwise. | | `data.verified_by` | string \| null | yes | The person who confirmed the change landed; null until verified. | | `data.verified_at` | string \| null | yes | When it was verified, ISO 8601; null until then. | | `data.version` | integer | yes | Its approved version: 0 while never approved. | | `data.approved_at` | string \| null | yes | When the approval carried, ISO 8601; null until then. | | `data.approved_revision_id` | string \| null | yes | The revision the approval froze; null until then. | | `data.position` | integer | yes | Its place among the project's change requests, from zero. | | `data.created_at` | string \| null | yes | When the request was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the request last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `change-request:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when a record under `affected` is hidden, gone or another project's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, the baseline or the stakeholder is not the project's, or a record under `affected` is of a family a request cannot touch. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a change request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/restore-a-version-of-a-change-request `POST /v1/change-requests/{changeRequest}/revisions/{revision}/restore` ```bash curl -X POST https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID/revisions/$REVISION_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the version's captured columns back onto the request, status included, and saves. The save writes a new version with the reason `restore` that names the one put back in `restored_from_id`; nothing earlier is rewritten. Answers `201` with the version written and emits `change_request.restored` with both version numbers. - Requires ability: `change-request:update` - Fires events: `change_request.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The request, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version the restore wrote, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `change-request:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the version is not the request's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the version is identical to the current record, so there is nothing to restore. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a change request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/update-a-change-request `PATCH /v1/change-requests/{changeRequest}` ```bash curl -X PATCH https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"impact_level": "medium", "impact_schedule_days": 5, "impact_effort_hours": 24, "impact_risk": "

The renewal job runs unattended.

"}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. `affected`, when sent, replaces the records the request touches in full. The words and the impact are edited until the approvers decide: a request approved, rejected or past the gate answers `422`. The status does not change here; the status, verify and analyse endpoints own it. A save that changes a captured column writes a new version. Answers `200` with the request and emits `change_request.updated` with the columns that changed, each with its old and new value, and `affected` before and after when that list changed; an update that changes nothing answers `200` and emits nothing. - Requires ability: `change-request:update` - Fires events: `change_request.updated` - MCP tools: `update_change_request` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The request, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A change request to raise or change. Only `title` is required on a create; the reason defaults to `new_requirement` and the priority to `medium`. On an update, a field left out keeps its value and a field sent as null is cleared; `affected`, when sent, replaces the records the request touches; the status moves through its own endpoints. A guest's token may raise a request but not write its impact. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | yes | What the change is called, 3 to 200 characters. | | `description` | string \| null | no | What is asked for and why, as HTML; filtered to a safe subset when read. | | `reason` | `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt`, `scope_reduction` | no | Why it was raised: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. Defaults to `new_requirement`. | | `stakeholder_id` | string \| null (uuid) | no | The stakeholder who asked, a stakeholder of the project; that seat co-signs the impact when an account is linked to it. Null for none. | | `baseline_id` | string \| null (uuid) | no | The baseline the change is raised against, a baseline of the project; its items among the records touched are unlocked when the request is approved. Null for none. | | `affected` | array \| null | no | The records the request touches, up to 200. Sent, they replace the list; left out, the list stays. | | `affected[].type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The record's family: `epic`, `work-item`, `requirement`, `business-rule` or `use-case`. | | `affected[].id` | string (uuid) | yes | The record's id. | | `impact_scope` | string \| null | no | What the change touches, as HTML. Ignored for a guest. | | `impact_schedule_days` | integer \| null | no | Days the schedule moves, -3650 to 3650; negative when it shortens. Ignored for a guest. | | `impact_effort_hours` | number \| null | no | Hours of effort, 0 to 999,999.99. Ignored for a guest. | | `impact_cost` | number \| null | no | The cost in the project's currency, 0 to 9,999,999,999.99. Ignored for a guest. | | `impact_risk` | string \| null | no | What the change risks, as HTML. Ignored for a guest. | | `impact_level` | `low`, `medium`, `high` \| null | no | How hard it hits the project: `low`, `medium` or `high`; null until judged. Ignored for a guest. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | How urgent: `lowest`, `low`, `medium`, `high` or `highest`. Defaults to `medium`. | ### Responses - **200**: The request. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The request's id. | | `data.project_id` | string | yes | The project the request belongs to. | | `data.number` | integer | yes | Its number in the change requests' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-CR-2`. | | `data.title` | string | yes | What the change is called. | | `data.description` | string \| null | yes | What is asked for and why, as HTML filtered to a safe subset; null when none. | | `data.reason` | string | yes | Why it was raised: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. | | `data.requested_by` | string \| null | yes | The person who raised it; null when that account is gone. | | `data.requester_name` | string \| null | yes | Their name, when it was loaded; null otherwise. | | `data.stakeholder_id` | string \| null | yes | The stakeholder who asked, when the client did; that seat co-signs the impact. Null otherwise. | | `data.stakeholder_name` | string \| null | yes | That stakeholder's name, when it was loaded; null otherwise. | | `data.baseline_id` | string \| null | yes | The baseline the change is raised against; null when none. | | `data.baseline_name` | string \| null | yes | That baseline's name, when it was loaded; null otherwise. | | `data.affected` | array of object | no | The records the request touches, when they were loaded. | | `data.affected[].artefact_type` | string | yes | The record's family. | | `data.affected[].artefact_id` | string | yes | The record's id. | | `data.affected[].reference` | string \| null | yes | The record's key; null when the record is gone or hidden. | | `data.affected[].title` | string \| null | yes | The record's title; null when the record is gone or hidden. | | `data.impact_scope` | string \| null | yes | What the change touches, as HTML; null until analysed. | | `data.impact_schedule_days` | integer \| null | yes | Days the schedule moves; negative when it shortens. Null until analysed. | | `data.impact_effort_hours` | string \| null | no | Hours of effort, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_cost` | string \| null | no | The cost, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_risk` | string \| null | yes | What the change risks, as HTML; null until analysed. | | `data.impact_level` | string \| null | yes | How hard it hits the project: `low`, `medium` or `high`; null until analysed. | | `data.ai_impact_draft` | string \| null | no | Heimdall's reading, as HTML; null until asked for. Left out for a guest. | | `data.analysed_at` | string \| null | yes | When Heimdall last read it, ISO 8601; null when never. | | `data.priority` | string | yes | How urgent: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.status` | string | yes | Where it stands: `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified` or `closed`. | | `data.decision_note` | string \| null | yes | The approvers' words when it was refused; null otherwise. | | `data.verified_by` | string \| null | yes | The person who confirmed the change landed; null until verified. | | `data.verified_at` | string \| null | yes | When it was verified, ISO 8601; null until then. | | `data.version` | integer | yes | Its approved version: 0 while never approved. | | `data.approved_at` | string \| null | yes | When the approval carried, ISO 8601; null until then. | | `data.approved_revision_id` | string \| null | yes | The revision the approval froze; null until then. | | `data.position` | integer | yes | Its place among the project's change requests, from zero. | | `data.created_at` | string \| null | yes | When the request was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the request last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `change-request:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when a record under `affected` is hidden, gone or another project's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, the baseline or the stakeholder is not the project's, or the request was decided already. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Verify a change request Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/verify-a-change-request `POST /v1/change-requests/{changeRequest}/verify` ```bash curl -X POST https://api.example.test/v1/change-requests/$CHANGE_REQUEST_ID/verify \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Confirms that an implemented change landed as asked: the request moves to `verified` and records who verified it and when. The move writes a version with the reason `transition`. Answers `200` with the request and emits `change_request.status_changed`; a request already verified answers `200` and emits nothing. - Requires ability: `change-request:verify` - Fires events: `change_request.status_changed` - MCP tools: `update_change_request` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `changeRequest` | path | string (uuid) | yes | The request, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The request, verified. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The request's id. | | `data.project_id` | string | yes | The project the request belongs to. | | `data.number` | integer | yes | Its number in the change requests' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-CR-2`. | | `data.title` | string | yes | What the change is called. | | `data.description` | string \| null | yes | What is asked for and why, as HTML filtered to a safe subset; null when none. | | `data.reason` | string | yes | Why it was raised: `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction`. | | `data.requested_by` | string \| null | yes | The person who raised it; null when that account is gone. | | `data.requester_name` | string \| null | yes | Their name, when it was loaded; null otherwise. | | `data.stakeholder_id` | string \| null | yes | The stakeholder who asked, when the client did; that seat co-signs the impact. Null otherwise. | | `data.stakeholder_name` | string \| null | yes | That stakeholder's name, when it was loaded; null otherwise. | | `data.baseline_id` | string \| null | yes | The baseline the change is raised against; null when none. | | `data.baseline_name` | string \| null | yes | That baseline's name, when it was loaded; null otherwise. | | `data.affected` | array of object | no | The records the request touches, when they were loaded. | | `data.affected[].artefact_type` | string | yes | The record's family. | | `data.affected[].artefact_id` | string | yes | The record's id. | | `data.affected[].reference` | string \| null | yes | The record's key; null when the record is gone or hidden. | | `data.affected[].title` | string \| null | yes | The record's title; null when the record is gone or hidden. | | `data.impact_scope` | string \| null | yes | What the change touches, as HTML; null until analysed. | | `data.impact_schedule_days` | integer \| null | yes | Days the schedule moves; negative when it shortens. Null until analysed. | | `data.impact_effort_hours` | string \| null | no | Hours of effort, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_cost` | string \| null | no | The cost, as a decimal string. Null until analysed; left out for a guest. | | `data.impact_risk` | string \| null | yes | What the change risks, as HTML; null until analysed. | | `data.impact_level` | string \| null | yes | How hard it hits the project: `low`, `medium` or `high`; null until analysed. | | `data.ai_impact_draft` | string \| null | no | Heimdall's reading, as HTML; null until asked for. Left out for a guest. | | `data.analysed_at` | string \| null | yes | When Heimdall last read it, ISO 8601; null when never. | | `data.priority` | string | yes | How urgent: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.status` | string | yes | Where it stands: `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified` or `closed`. | | `data.decision_note` | string \| null | yes | The approvers' words when it was refused; null otherwise. | | `data.verified_by` | string \| null | yes | The person who confirmed the change landed; null until verified. | | `data.verified_at` | string \| null | yes | When it was verified, ISO 8601; null until then. | | `data.version` | integer | yes | Its approved version: 0 while never approved. | | `data.approved_at` | string \| null | yes | When the approval carried, ISO 8601; null until then. | | `data.approved_revision_id` | string \| null | yes | The revision the approval froze; null until then. | | `data.position` | integer | yes | Its place among the project's change requests, from zero. | | `data.created_at` | string \| null | yes | When the request was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the request last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `change-request:verify`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the request is not implemented. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Archive a client Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/archive-a-client `POST /v1/clients/{client}/archive` ```bash curl -X POST https://api.example.test/v1/clients/$CLIENT_ID/archive \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the client away: it leaves the Clients page's default view and the default listing, while its projects and its history stay exactly as they were. Answers `200` with the client, `archived_at` set, and emits `client.archived`. - Requires ability: `client:update` - Fires events: `client.archived` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `client` | path | string (uuid) | yes | The client, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The client with its archive stamp. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The client's id, the value `client_id` carries on a project. | | `data.team_id` | string | yes | The team the client belongs to. | | `data.name` | string | yes | The client's name, unique within the team. | | `data.legal_name` | string \| null | yes | The registered name, for documents; null when none. | | `data.website` | string \| null | yes | The client's site; null when none. | | `data.industry` | string \| null | yes | The line of business, free text; null when none. | | `data.country` | string \| null | yes | A two-letter country code; null when none. | | `data.timezone` | string \| null | yes | The timezone the client works in; null when none. | | `data.primary_contact_name` | string \| null | yes | Who speaks for the client; null when none. | | `data.primary_contact_email` | string \| null | yes | Their email address; null when none. | | `data.notes` | string \| array of any \| null | yes | What the team should know about working with the client, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the relationship stands: `prospect`, `active`, `dormant` or `churned`. | | `data.projects_count` | integer | no | How many projects the token can see name this client; present on list responses. | | `data.archived_at` | string \| null | yes | When the client was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the client was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the client last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role does not carry `client:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the client is archived already. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Create a client Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/create-a-client `POST /v1/clients` ```bash curl -X POST https://api.example.test/v1/clients \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Retail", "legal_name": "Acme Retail (Pvt) Ltd", "website": "https://acme.example", "country": "LK", "timezone": "Asia/Colombo", "primary_contact_name": "Jane Perera", "primary_contact_email": "jane@acme.example" }' ``` Only `name` is required; it must be unique within the team, case-insensitively. `status` defaults to `active`. The client is created in the team the token acts in. Answers `201` with the client and emits `client.created`. - Requires ability: `client:create` - Fires events: `client.created` - MCP tools: `create_client` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A client to create or change. Only `name` is required on a create; on an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The client's name, 2 to 100 characters, unique within the team. | | `legal_name` | string \| null | no | The registered name, up to 160 characters. | | `website` | string \| null (uri) | no | The client's site as a full URL, up to 255 characters. | | `industry` | string \| null | no | The line of business, up to 80 characters. | | `country` | string \| null | no | A two-letter upper-case country code, such as `LK`. | | `timezone` | string \| null | no | A timezone identifier, such as `Asia/Colombo`. | | `primary_contact_name` | string \| null | no | Who speaks for the client, up to 120 characters. | | `primary_contact_email` | string \| null (email) | no | Their email address. | | `notes` | string \| null | no | What the team should know, as HTML; filtered to a safe subset when read. | | `status` | `prospect`, `active`, `dormant`, `churned` | no | Where the relationship stands: `prospect`, `active`, `dormant` or `churned`. Defaults to `active` on a create. | ### Responses - **201**: The client as a 201. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The client's id, the value `client_id` carries on a project. | | `data.team_id` | string | yes | The team the client belongs to. | | `data.name` | string | yes | The client's name, unique within the team. | | `data.legal_name` | string \| null | yes | The registered name, for documents; null when none. | | `data.website` | string \| null | yes | The client's site; null when none. | | `data.industry` | string \| null | yes | The line of business, free text; null when none. | | `data.country` | string \| null | yes | A two-letter country code; null when none. | | `data.timezone` | string \| null | yes | The timezone the client works in; null when none. | | `data.primary_contact_name` | string \| null | yes | Who speaks for the client; null when none. | | `data.primary_contact_email` | string \| null | yes | Their email address; null when none. | | `data.notes` | string \| array of any \| null | yes | What the team should know about working with the client, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the relationship stands: `prospect`, `active`, `dormant` or `churned`. | | `data.projects_count` | integer | no | How many projects the token can see name this client; present on list responses. | | `data.archived_at` | string \| null | yes | When the client was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the client was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the client last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role does not carry `client:create`. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, or when another client of the team already uses the name. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a client Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/delete-a-client `DELETE /v1/clients/{client}` ```bash curl -X DELETE https://api.example.test/v1/clients/$CLIENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a client that no project names. Returns `204 No Content` and emits `client.deleted`. A client with a project, archived or deleted ones included, is refused: archive it instead, or move the projects to another client first. - Requires ability: `client:delete` - Fires events: `client.deleted` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `client` | path | string (uuid) | yes | The client, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role does not carry `client:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when a project still names the client (`error.context.projects` counts them). - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a client Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/get-a-client `GET /v1/clients/{client}` ```bash curl https://api.example.test/v1/clients/$CLIENT_ID \ -H "Authorization: Bearer $TOKEN" ``` One client. A client outside the token's team, or one a guest's projects do not name, is `404 RESOURCE_NOT_FOUND`. - `status` runs `prospect`, `active`, `dormant` and `churned`; it is set by hand and says where the relationship stands, not whether the client is archived, which `archived_at` says. - `notes` is HTML filtered to a safe subset; `country` is a two-letter code and `timezone` an identifier such as `Asia/Colombo`. - Requires ability: `client:view` - MCP tools: `get_client` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `client` | path | string (uuid) | yes | The client, resolved by the route binder. | ### Responses - **200**: The client. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The client's id, the value `client_id` carries on a project. | | `data.team_id` | string | yes | The team the client belongs to. | | `data.name` | string | yes | The client's name, unique within the team. | | `data.legal_name` | string \| null | yes | The registered name, for documents; null when none. | | `data.website` | string \| null | yes | The client's site; null when none. | | `data.industry` | string \| null | yes | The line of business, free text; null when none. | | `data.country` | string \| null | yes | A two-letter country code; null when none. | | `data.timezone` | string \| null | yes | The timezone the client works in; null when none. | | `data.primary_contact_name` | string \| null | yes | Who speaks for the client; null when none. | | `data.primary_contact_email` | string \| null | yes | Their email address; null when none. | | `data.notes` | string \| array of any \| null | yes | What the team should know about working with the client, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the relationship stands: `prospect`, `active`, `dormant` or `churned`. | | `data.projects_count` | integer | no | How many projects the token can see name this client; present on list responses. | | `data.archived_at` | string \| null | yes | When the client was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the client was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the client last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Clients API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/clients A **client** is an organisation the team does projects for: its names, its primary contact, where it works from and where the relationship stands. A project names its client through `client_id`, so the client exists first; a client with a project, archived or deleted ones included, is archived rather than deleted, so a project never loses the client it was done for. The reads here see the clients of the token's team; a client guest sees only the clients of the projects they were invited to. A client outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`, so the API cannot be used to learn that a client exists. Every write runs the same action the Clients page runs, under the team permissions, and announces itself as a `client.*` event. ## Endpoints - `GET /v1/clients` — [List clients](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/list-clients) - `POST /v1/clients` — [Create a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/create-a-client) - `GET /v1/clients/{client}` — [Get a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/get-a-client) - `PATCH /v1/clients/{client}` — [Update a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/update-a-client) - `DELETE /v1/clients/{client}` — [Delete a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/delete-a-client) - `POST /v1/clients/{client}/archive` — [Archive a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/archive-a-client) - `POST /v1/clients/{client}/restore` — [Restore a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/restore-a-client) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The engagements done for a client, filtered by client_id. - [Teams API](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams): The team a client belongs to. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on a client and who did it. - [client.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client): The lifecycle of a client: creation, edits, archiving, restoring and deletion. --- # List clients Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/list-clients `GET /v1/clients` ```bash curl "https://api.example.test/v1/clients?status=active" \ -H "Authorization: Bearer $TOKEN" ``` Every client the token can see, in name order, 25 per page (`per_page` 1 to 100). `sort_by` takes `name`, `status`, `created_at` or `updated_at`. `status` and `search` narrow the page; `archived=1` lists archived clients only and `archived=0` live ones only, with both listed when it is omitted. Each row carries `projects_count`, how many projects the token can see name the client. - Requires ability: `client:view-any` - MCP tools: `list_clients` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | `prospect`, `active`, `dormant`, `churned` \| null | no | Narrow to one status: `prospect`, `active`, `dormant` or `churned`. An unknown status is refused. | | `search` | query | string \| null | no | Words the name, the legal name or the primary contact email must contain, up to 120 characters. | | `archived` | query | boolean \| null | no | `1` lists archived clients only, `0` live ones only; omit for both. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: The page, with each client's project count. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The client's id, the value `client_id` carries on a project. | | `data[].team_id` | string | yes | The team the client belongs to. | | `data[].name` | string | yes | The client's name, unique within the team. | | `data[].legal_name` | string \| null | yes | The registered name, for documents; null when none. | | `data[].website` | string \| null | yes | The client's site; null when none. | | `data[].industry` | string \| null | yes | The line of business, free text; null when none. | | `data[].country` | string \| null | yes | A two-letter country code; null when none. | | `data[].timezone` | string \| null | yes | The timezone the client works in; null when none. | | `data[].primary_contact_name` | string \| null | yes | Who speaks for the client; null when none. | | `data[].primary_contact_email` | string \| null | yes | Their email address; null when none. | | `data[].notes` | string \| array of any \| null | yes | What the team should know about working with the client, as HTML filtered to a safe subset; null when none. | | `data[].status` | string | yes | Where the relationship stands: `prospect`, `active`, `dormant` or `churned`. | | `data[].projects_count` | integer | no | How many projects the token can see name this client; present on list responses. | | `data[].archived_at` | string \| null | yes | When the client was archived, ISO 8601; null while live. | | `data[].created_at` | string \| null | yes | When the client was created, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the client last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is not one of the four statuses, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a client Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/restore-a-client `POST /v1/clients/{client}/restore` ```bash curl -X POST https://api.example.test/v1/clients/$CLIENT_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Brings an archived client back. Answers `200` with the client, `archived_at` null again, and emits `client.restored`. - Requires ability: `client:update` - Fires events: `client.restored` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `client` | path | string (uuid) | yes | The client, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The client, live again. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The client's id, the value `client_id` carries on a project. | | `data.team_id` | string | yes | The team the client belongs to. | | `data.name` | string | yes | The client's name, unique within the team. | | `data.legal_name` | string \| null | yes | The registered name, for documents; null when none. | | `data.website` | string \| null | yes | The client's site; null when none. | | `data.industry` | string \| null | yes | The line of business, free text; null when none. | | `data.country` | string \| null | yes | A two-letter country code; null when none. | | `data.timezone` | string \| null | yes | The timezone the client works in; null when none. | | `data.primary_contact_name` | string \| null | yes | Who speaks for the client; null when none. | | `data.primary_contact_email` | string \| null | yes | Their email address; null when none. | | `data.notes` | string \| array of any \| null | yes | What the team should know about working with the client, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the relationship stands: `prospect`, `active`, `dormant` or `churned`. | | `data.projects_count` | integer | no | How many projects the token can see name this client; present on list responses. | | `data.archived_at` | string \| null | yes | When the client was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the client was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the client last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role does not carry `client:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the client is not archived. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a client Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/update-a-client `PATCH /v1/clients/{client}` ```bash curl -X PATCH https://api.example.test/v1/clients/$CLIENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "dormant", "primary_contact_email": "ops@acme.example"}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. The name stays unique within the team. Answers `200` with the client and emits `client.updated` with the columns that changed, each with its old and new value; an update that changes nothing answers `200` and emits nothing. - Requires ability: `client:update` - Fires events: `client.updated` - MCP tools: `update_client` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `client` | path | string (uuid) | yes | The client, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A client to create or change. Only `name` is required on a create; on an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The client's name, 2 to 100 characters, unique within the team. | | `legal_name` | string \| null | no | The registered name, up to 160 characters. | | `website` | string \| null (uri) | no | The client's site as a full URL, up to 255 characters. | | `industry` | string \| null | no | The line of business, up to 80 characters. | | `country` | string \| null | no | A two-letter upper-case country code, such as `LK`. | | `timezone` | string \| null | no | A timezone identifier, such as `Asia/Colombo`. | | `primary_contact_name` | string \| null | no | Who speaks for the client, up to 120 characters. | | `primary_contact_email` | string \| null (email) | no | Their email address. | | `notes` | string \| null | no | What the team should know, as HTML; filtered to a safe subset when read. | | `status` | `prospect`, `active`, `dormant`, `churned` | no | Where the relationship stands: `prospect`, `active`, `dormant` or `churned`. Defaults to `active` on a create. | ### Responses - **200**: The updated client. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The client's id, the value `client_id` carries on a project. | | `data.team_id` | string | yes | The team the client belongs to. | | `data.name` | string | yes | The client's name, unique within the team. | | `data.legal_name` | string \| null | yes | The registered name, for documents; null when none. | | `data.website` | string \| null | yes | The client's site; null when none. | | `data.industry` | string \| null | yes | The line of business, free text; null when none. | | `data.country` | string \| null | yes | A two-letter country code; null when none. | | `data.timezone` | string \| null | yes | The timezone the client works in; null when none. | | `data.primary_contact_name` | string \| null | yes | Who speaks for the client; null when none. | | `data.primary_contact_email` | string \| null | yes | Their email address; null when none. | | `data.notes` | string \| array of any \| null | yes | What the team should know about working with the client, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the relationship stands: `prospect`, `active`, `dormant` or `churned`. | | `data.projects_count` | integer | no | How many projects the token can see name this client; present on list responses. | | `data.archived_at` | string \| null | yes | When the client was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the client was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the client last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role does not carry `client:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, or when the new name is already used by another client of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a comment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/delete-a-comment `DELETE /v1/comments/{comment}` ```bash curl -X DELETE https://api.example.test/v1/comments/$COMMENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Deletes a comment the token's holder wrote, or anyone's when the holder's role carries `comment:delete`, with the replies under it. Returns `204 No Content` and emits `comment.deleted` once, for the comment named. - Requires ability: `comment:delete` - Fires events: `comment.deleted` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `comment` | path | string (uuid) | yes | The comment. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the comment is someone else's and the role does not carry `comment:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the comment is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Edit a comment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/edit-a-comment `PATCH /v1/comments/{comment}` ```bash curl -X PATCH https://api.example.test/v1/comments/$COMMENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "body": "

After the discount, per CRM-BR-7. @Sam Lee can you confirm?

" }' ``` Rewrites the words of a comment the token's holder wrote, or anyone's when the holder's role carries `comment:update`. The comment is marked edited, anyone newly named with `@` is told, and `comment.updated` goes out. - Requires ability: `comment:update` - Fires events: `comment.updated` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `comment` | path | string (uuid) | yes | The comment. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) New words for a comment. The comment is marked edited; anyone newly named with `@` is told. | Field | Type | Required | Description | | --- | --- | --- | --- | | `body` | string | yes | The words, as HTML or plain text; sanitised on read. Up to 20,000 characters. | ### Responses - **200**: The comment. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The comment's id. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.artefact_type` | string | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data.artefact_id` | string | yes | The artefact's id. | | `data.parent_id` | string \| null | yes | The first comment of the thread this one answers; null for a first comment. | | `data.author_id` | string \| null | yes | The person who wrote it; null when the account is gone. | | `data.author_name` | string \| null | yes | The author's name, when the author was loaded; null otherwise. | | `data.body` | string | yes | The words, as sanitised HTML. | | `data.internal` | boolean | yes | Whether the comment is internal: a client guest never reads it. | | `data.mentions` | array of any | yes | The ids of the people the comment names with `@`. | | `data.resolved` | boolean | yes | Whether the thread was marked resolved. | | `data.resolved_at` | string \| null | yes | When it was resolved, ISO 8601; null while open. | | `data.resolved_by` | string \| null | yes | The person who resolved it; null while open or when the account is gone. | | `data.edited_at` | string \| null | yes | When the words were last rewritten, ISO 8601; null when never. | | `data.created_at` | string \| null | yes | When the comment was written, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the comment is someone else's and the role does not carry `comment:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the comment is of a project the token may not see, on an artefact hidden from it, or internal and the token is a client guest's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the body is empty or over 20,000 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Comments API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/comments A **comment** is a note on one artefact of a project: a question on a requirement, an answer under it, a finding on a story, a decision taken in the margin of a RAID item. A comment answers at most one earlier comment, so a thread is one level deep and reads as a conversation. A comment is either open to everyone who sees the artefact or **internal**, which a client guest never reads; a reply to an internal comment is internal too. People named with `@` and their full name (or their first name when it is unambiguous among those who may read the comment) are told; so are the artefact's followers. The first comment of a thread can be **resolved** and reopened. Nine artefact types carry comments today: epics, work items, requirements, business rules, use cases, RAID items, decisions, data entities and glossary terms. A token reads the comments of the artefacts its holder may see, a client guest's token the open ones only and only on the agreed record. Posting needs `comment:create` on the project and, for a guest, the project's `guest_can_comment` setting; an internal comment needs `comment:internal`. The author edits, resolves and deletes their own comments; `comment:update` and `comment:delete` let a moderator do the same for anyone's. Every write runs the same action the comments panel runs and announces itself as a `comment.*` event. ## Endpoints - `GET /v1/projects/{project}/comments` — [List an artefact's comments](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/list-an-artefacts-comments) - `POST /v1/projects/{project}/comments` — [Post a comment](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/post-a-comment) - `PATCH /v1/comments/{comment}` — [Edit a comment](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/edit-a-comment) - `DELETE /v1/comments/{comment}` — [Delete a comment](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/delete-a-comment) - `POST /v1/comments/{comment}/resolve` — [Resolve a thread](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/resolve-a-thread) - `POST /v1/comments/{comment}/reopen` — [Reopen a thread](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/reopen-a-thread) ## Related - [Watchers API](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers): Who is told of a comment. - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items): [requirements](/api/v1/reference/requirements), [business rules](/api/v1/reference/business-rules), [use cases](/api/v1/reference/use-cases), [the RAID log](/api/v1/reference/raid-log) and [decisions](/api/v1/reference/decisions): the artefacts the comments hang on. - [comment.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment): The conversations on a project's artefacts: a comment posted, edited, resolved, reopened and deleted. --- # List an artefact's comments Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/list-an-artefacts-comments `GET /v1/projects/{project}/comments` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/comments?artefact_type=work-item&artefact_id=$WORK_ITEM_ID" \ -H "Authorization: Bearer $TOKEN" ``` The conversation on one artefact, oldest first, each comment with its author's name, the words as sanitised HTML, whether it is internal, the people it names, the first comment it answers when a reply, and whether and by whom the thread was resolved. `artefact_type` and `artefact_id` are required; the artefact must be one the token may see. A client guest's token receives the open comments only. - Requires ability: `comment:view-any` - MCP tools: `list_comments` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `artefact_type` | query | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. Required. | | `artefact_id` | query | string (uuid) | yes | The artefact's id. Required. | ### Responses - **200**: Array of `CommentResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The comment's id. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].artefact_type` | string | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data[].artefact_id` | string | yes | The artefact's id. | | `data[].parent_id` | string \| null | yes | The first comment of the thread this one answers; null for a first comment. | | `data[].author_id` | string \| null | yes | The person who wrote it; null when the account is gone. | | `data[].author_name` | string \| null | yes | The author's name, when the author was loaded; null otherwise. | | `data[].body` | string | yes | The words, as sanitised HTML. | | `data[].internal` | boolean | yes | Whether the comment is internal: a client guest never reads it. | | `data[].mentions` | array of any | yes | The ids of the people the comment names with `@`. | | `data[].resolved` | boolean | yes | Whether the thread was marked resolved. | | `data[].resolved_at` | string \| null | yes | When it was resolved, ISO 8601; null while open. | | `data[].resolved_by` | string \| null | yes | The person who resolved it; null while open or when the account is gone. | | `data[].edited_at` | string \| null | yes | When the words were last rewritten, ISO 8601; null when never. | | `data[].created_at` | string \| null | yes | When the comment was written, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `comment:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the artefact is hidden from the token, missing or of another project. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `artefact_type` is not one of the nine or `artefact_id` is not a uuid. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Post a comment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/post-a-comment `POST /v1/projects/{project}/comments` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/comments \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "artefact_type": "work-item", "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "body": "

After the discount: the rule CRM-BR-7 says the voucher comes last.

", "parent_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81" }' ``` Writes a comment on an artefact of the project as the token's holder, who becomes a follower of the artefact. `body` is HTML or plain text, sanitised on read. `parent_id` names the first comment of the thread the comment answers; it must be on the same artefact. `internal` keeps the comment from client guests and needs `comment:internal`; a reply to an internal comment is internal whatever is sent. The people named and the other followers are told. Answers `201` with the comment and emits `comment.created`. - Requires ability: `comment:create` - Fires events: `comment.created` - MCP tools: `comment_on_item` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A comment to post: the artefact by type and id, the words, whether the comment is internal and, for a reply, the first comment of the thread it answers. People named with `@` and their full name, or their first name when it is unambiguous, are told. | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `artefact_id` | string (uuid) | yes | The artefact's id, one of the project's. | | `body` | string | yes | The words, as HTML or plain text; sanitised on read. Up to 20,000 characters. | | `internal` | boolean | no | Whether the comment is internal, never shown to a client guest. Needs `comment:internal`; false by default. A reply to an internal comment is internal whatever is sent. | | `parent_id` | string \| null (uuid) | no | The first comment of the thread this one answers, on the same artefact; omit for a first comment. | ### Responses - **201**: The comment, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The comment's id. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.artefact_type` | string | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data.artefact_id` | string | yes | The artefact's id. | | `data.parent_id` | string \| null | yes | The first comment of the thread this one answers; null for a first comment. | | `data.author_id` | string \| null | yes | The person who wrote it; null when the account is gone. | | `data.author_name` | string \| null | yes | The author's name, when the author was loaded; null otherwise. | | `data.body` | string | yes | The words, as sanitised HTML. | | `data.internal` | boolean | yes | Whether the comment is internal: a client guest never reads it. | | `data.mentions` | array of any | yes | The ids of the people the comment names with `@`. | | `data.resolved` | boolean | yes | Whether the thread was marked resolved. | | `data.resolved_at` | string \| null | yes | When it was resolved, ISO 8601; null while open. | | `data.resolved_by` | string \| null | yes | The person who resolved it; null while open or when the account is gone. | | `data.edited_at` | string \| null | yes | When the words were last rewritten, ISO 8601; null when never. | | `data.created_at` | string \| null | yes | When the comment was written, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `comment:create`, the holder is a client guest on a project whose `guest_can_comment` setting is off, or the comment is internal and the role does not carry `comment:internal`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the artefact is hidden from the token, missing or of another project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the type is unknown, an id is not a uuid, the body is empty or over 20,000 characters, or the parent is missing, a reply itself or on another artefact. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Reopen a thread Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/reopen-a-thread `POST /v1/comments/{comment}/reopen` ```bash curl -X POST https://api.example.test/v1/comments/$COMMENT_ID/reopen \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Marks a resolved thread open again. Emits `comment.reopened`; reopening an open thread changes nothing and announces nothing. - Requires ability: `comment:update` - Fires events: `comment.reopened` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `comment` | path | string (uuid) | yes | The first comment of the thread. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The comment. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The comment's id. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.artefact_type` | string | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data.artefact_id` | string | yes | The artefact's id. | | `data.parent_id` | string \| null | yes | The first comment of the thread this one answers; null for a first comment. | | `data.author_id` | string \| null | yes | The person who wrote it; null when the account is gone. | | `data.author_name` | string \| null | yes | The author's name, when the author was loaded; null otherwise. | | `data.body` | string | yes | The words, as sanitised HTML. | | `data.internal` | boolean | yes | Whether the comment is internal: a client guest never reads it. | | `data.mentions` | array of any | yes | The ids of the people the comment names with `@`. | | `data.resolved` | boolean | yes | Whether the thread was marked resolved. | | `data.resolved_at` | string \| null | yes | When it was resolved, ISO 8601; null while open. | | `data.resolved_by` | string \| null | yes | The person who resolved it; null while open or when the account is gone. | | `data.edited_at` | string \| null | yes | When the words were last rewritten, ISO 8601; null when never. | | `data.created_at` | string \| null | yes | When the comment was written, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the comment is someone else's and the role does not carry `comment:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the comment is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the comment is a reply. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Resolve a thread Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/resolve-a-thread `POST /v1/comments/{comment}/resolve` ```bash curl -X POST https://api.example.test/v1/comments/$COMMENT_ID/resolve \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Marks the first comment of a thread resolved, by the token's holder; its replies follow it. Emits `comment.resolved`; resolving a resolved thread changes nothing and announces nothing. - Requires ability: `comment:update` - Fires events: `comment.resolved` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `comment` | path | string (uuid) | yes | The first comment of the thread. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The comment. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The comment's id. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.artefact_type` | string | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data.artefact_id` | string | yes | The artefact's id. | | `data.parent_id` | string \| null | yes | The first comment of the thread this one answers; null for a first comment. | | `data.author_id` | string \| null | yes | The person who wrote it; null when the account is gone. | | `data.author_name` | string \| null | yes | The author's name, when the author was loaded; null otherwise. | | `data.body` | string | yes | The words, as sanitised HTML. | | `data.internal` | boolean | yes | Whether the comment is internal: a client guest never reads it. | | `data.mentions` | array of any | yes | The ids of the people the comment names with `@`. | | `data.resolved` | boolean | yes | Whether the thread was marked resolved. | | `data.resolved_at` | string \| null | yes | When it was resolved, ISO 8601; null while open. | | `data.resolved_by` | string \| null | yes | The person who resolved it; null while open or when the account is gone. | | `data.edited_at` | string \| null | yes | When the words were last rewritten, ISO 8601; null when never. | | `data.created_at` | string \| null | yes | When the comment was written, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the comment is someone else's and the role does not carry `comment:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the comment is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the comment is a reply. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Archive a component Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/components/archive-a-component `POST /v1/components/{component}/archive` ```bash curl -X POST https://api.example.test/v1/components/$COMPONENT_ID/archive \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the component away: it leaves the Components page's default view and the default listing, while its environments and its history stay exactly as they were. Answers `200` with the component, `archived_at` set, and emits `component.archived`. - Requires ability: `component:update` - Fires events: `component.archived` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `component` | path | string (uuid) | yes | The component, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The component with its archive stamp. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The component's id, the value `component_id` carries on an environment. | | `data.project_id` | string | yes | The project the component belongs to. | | `data.name` | string | yes | The component's name, unique within the project. | | `data.slug` | string | yes | The slug a version string names the component by, unique within the project. | | `data.kind` | string | yes | What the component is: `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure` or `other`. | | `data.description` | string \| array of any \| null | yes | What the component does, as HTML filtered to a safe subset; null when none. | | `data.tech_stack` | array of any | yes | The technologies it is built with, as short tags. | | `data.owner_id` | string \| null | yes | The team member who answers for it; null when unowned. | | `data.position` | integer | yes | Its place among the project's components, from zero. | | `data.environments_count` | integer | no | How many environments run the component; present on list responses. | | `data.archived_at` | string \| null | yes | When the component was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the component was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the component last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `component:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the component is archived already. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Create a component Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/components/create-a-component `POST /v1/projects/{project}/components` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/components \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Public API", "kind": "api", "tech_stack": ["Laravel 13", "PostgreSQL 18"] }' ``` Only `name` is required; it must be unique within the project, case-insensitively. The slug is derived from the name when left out and suffixed until it is free; a typed slug that another component of the project uses is refused. `kind` defaults to `web_app`. The owner, when named, must be a member of the team. Answers `201` with the component and emits `component.created`. - Requires ability: `component:create` - Fires events: `component.created` - MCP tools: `create_component` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A component to create or change. Only `name` is required on a create; the slug is derived from the name when left out. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The component's name, 2 to 80 characters, unique within the project. | | `slug` | string \| null | no | The slug a version string names the component by (`web@1.4.0`): lower-case letters, digits and hyphens starting with a letter, 2 to 40 characters, unique within the project. Derived from the name when left out on a create. | | `kind` | `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure`, `other` | no | What the component is: `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure` or `other`. Defaults to `web_app` on a create. | | `description` | string \| null | no | What the component does, as HTML; filtered to a safe subset when read. | | `tech_stack` | array \| null | no | The technologies it is built with, as up to twenty short tags of at most 40 characters each. | | `owner_id` | string \| null (uuid) | no | The team member who answers for the component; null to leave it unowned. | ### Responses - **201**: The component as a 201. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The component's id, the value `component_id` carries on an environment. | | `data.project_id` | string | yes | The project the component belongs to. | | `data.name` | string | yes | The component's name, unique within the project. | | `data.slug` | string | yes | The slug a version string names the component by, unique within the project. | | `data.kind` | string | yes | What the component is: `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure` or `other`. | | `data.description` | string \| array of any \| null | yes | What the component does, as HTML filtered to a safe subset; null when none. | | `data.tech_stack` | array of any | yes | The technologies it is built with, as short tags. | | `data.owner_id` | string \| null | yes | The team member who answers for it; null when unowned. | | `data.position` | integer | yes | Its place among the project's components, from zero. | | `data.environments_count` | integer | no | How many environments run the component; present on list responses. | | `data.archived_at` | string \| null | yes | When the component was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the component was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the component last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `component:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when another component of the project already uses the name or the typed slug, or when the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a component Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/components/delete-a-component `DELETE /v1/components/{component}` ```bash curl -X DELETE https://api.example.test/v1/components/$COMPONENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a component no environment runs. Returns `204 No Content` and emits `component.deleted`. A component with environments is refused: archive it instead, or move the environments to another component first. - Requires ability: `component:delete` - Fires events: `component.deleted` - MCP tools: `delete_component` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `component` | path | string (uuid) | yes | The component, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `component:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when an environment still runs the component (`error.context.environments` counts them). - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a component Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/components/get-a-component `GET /v1/components/{component}` ```bash curl https://api.example.test/v1/components/$COMPONENT_ID \ -H "Authorization: Bearer $TOKEN" ``` One component. A component of a project the token may not see is `404 RESOURCE_NOT_FOUND`. - `kind` runs `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure` and `other`. - `tech_stack` is a list of short tags; `description` is HTML filtered to a safe subset. - Requires ability: `component:view` - MCP tools: `get_component` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `component` | path | string (uuid) | yes | The component, resolved by the route binder. | ### Responses - **200**: The component. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The component's id, the value `component_id` carries on an environment. | | `data.project_id` | string | yes | The project the component belongs to. | | `data.name` | string | yes | The component's name, unique within the project. | | `data.slug` | string | yes | The slug a version string names the component by, unique within the project. | | `data.kind` | string | yes | What the component is: `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure` or `other`. | | `data.description` | string \| array of any \| null | yes | What the component does, as HTML filtered to a safe subset; null when none. | | `data.tech_stack` | array of any | yes | The technologies it is built with, as short tags. | | `data.owner_id` | string \| null | yes | The team member who answers for it; null when unowned. | | `data.position` | integer | yes | Its place among the project's components, from zero. | | `data.environments_count` | integer | no | How many environments run the component; present on list responses. | | `data.archived_at` | string \| null | yes | When the component was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the component was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the component last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Components API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/components A **component** is one application a project is made of: the web app, the API, a mobile app, a background service, an integration, a database or the platform itself. Environments name the component they run, stories and requirements will tag the components they touch, and a release will carry a version per component; the **slug** is what a version string names the component by (`web@1.4.0`), so it is unique within the project and stays while the row exists. The reads here see the components of the projects the token may see. A component outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`, so the API cannot be used to learn that a component exists. Every write runs the same action the Components page runs, under the project permissions (the team owner, then the person's project role, then their team role), and announces itself as a `component.*` event. ## Endpoints - `GET /v1/components/{component}` — [Get a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/get-a-component) - `PATCH /v1/components/{component}` — [Update a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/update-a-component) - `DELETE /v1/components/{component}` — [Delete a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/delete-a-component) - `GET /v1/projects/{project}/components` — [List components](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/list-components) - `POST /v1/projects/{project}/components` — [Create a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/create-a-component) - `POST /v1/components/{component}/archive` — [Archive a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/archive-a-component) - `POST /v1/components/{component}/restore` — [Restore a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/restore-a-component) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project a component belongs to. - [Environments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments): Where a component runs. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on a component and who did it. - [component.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component): The lifecycle of a project's components: creation, edits, archiving, restoring and deletion. --- # List components Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/components/list-components `GET /v1/projects/{project}/components` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/components?kind=api" \ -H "Authorization: Bearer $TOKEN" ``` The components of one project in the project's order, 25 per page (`per_page` 1 to 100). `sort_by` takes `name`, `slug`, `kind`, `position`, `created_at` or `updated_at`. `kind` and `search` narrow the page; `archived=1` lists archived components only and `archived=0` live ones only, with both listed when it is omitted. Each row carries `environments_count`, how many environments run the component. - Requires ability: `component:view-any` - MCP tools: `list_components` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, resolved by the route binder. | | `kind` | query | `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure`, `other` \| null | no | Narrow to one kind, such as `web_app` or `api`. An unknown kind is refused. | | `search` | query | string \| null | no | Words the name or the slug must contain, up to 120 characters. | | `archived` | query | boolean \| null | no | `1` lists archived components only, `0` live ones only; omit for both. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: The page, with each component's environment count. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The component's id, the value `component_id` carries on an environment. | | `data[].project_id` | string | yes | The project the component belongs to. | | `data[].name` | string | yes | The component's name, unique within the project. | | `data[].slug` | string | yes | The slug a version string names the component by, unique within the project. | | `data[].kind` | string | yes | What the component is: `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure` or `other`. | | `data[].description` | string \| array of any \| null | yes | What the component does, as HTML filtered to a safe subset; null when none. | | `data[].tech_stack` | array of any | yes | The technologies it is built with, as short tags. | | `data[].owner_id` | string \| null | yes | The team member who answers for it; null when unowned. | | `data[].position` | integer | yes | Its place among the project's components, from zero. | | `data[].environments_count` | integer | no | How many environments run the component; present on list responses. | | `data[].archived_at` | string \| null | yes | When the component was archived, ISO 8601; null while live. | | `data[].created_at` | string \| null | yes | When the component was created, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the component last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `kind` is not one of the kinds, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a component Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/components/restore-a-component `POST /v1/components/{component}/restore` ```bash curl -X POST https://api.example.test/v1/components/$COMPONENT_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Brings an archived component back. Answers `200` with the component, `archived_at` null again, and emits `component.restored`. - Requires ability: `component:update` - Fires events: `component.restored` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `component` | path | string (uuid) | yes | The component, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The component, live again. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The component's id, the value `component_id` carries on an environment. | | `data.project_id` | string | yes | The project the component belongs to. | | `data.name` | string | yes | The component's name, unique within the project. | | `data.slug` | string | yes | The slug a version string names the component by, unique within the project. | | `data.kind` | string | yes | What the component is: `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure` or `other`. | | `data.description` | string \| array of any \| null | yes | What the component does, as HTML filtered to a safe subset; null when none. | | `data.tech_stack` | array of any | yes | The technologies it is built with, as short tags. | | `data.owner_id` | string \| null | yes | The team member who answers for it; null when unowned. | | `data.position` | integer | yes | Its place among the project's components, from zero. | | `data.environments_count` | integer | no | How many environments run the component; present on list responses. | | `data.archived_at` | string \| null | yes | When the component was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the component was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the component last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `component:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the component is not archived. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a component Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/components/update-a-component `PATCH /v1/components/{component}` ```bash curl -X PATCH https://api.example.test/v1/components/$COMPONENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"owner_id": "f0e1d2c3-b4a5-4968-8776-655443322110", "tech_stack": ["Laravel 13", "Livewire 4"]}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. A slug sent here replaces the current one and must be free within the project; a slug left out keeps its value, it is never re-derived. Answers `200` with the component and emits `component.updated` with the columns that changed, each with its old and new value; an update that changes nothing answers `200` and emits nothing. - Requires ability: `component:update` - Fires events: `component.updated` - MCP tools: `update_component` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `component` | path | string (uuid) | yes | The component, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A component to create or change. Only `name` is required on a create; the slug is derived from the name when left out. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The component's name, 2 to 80 characters, unique within the project. | | `slug` | string \| null | no | The slug a version string names the component by (`web@1.4.0`): lower-case letters, digits and hyphens starting with a letter, 2 to 40 characters, unique within the project. Derived from the name when left out on a create. | | `kind` | `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure`, `other` | no | What the component is: `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure` or `other`. Defaults to `web_app` on a create. | | `description` | string \| null | no | What the component does, as HTML; filtered to a safe subset when read. | | `tech_stack` | array \| null | no | The technologies it is built with, as up to twenty short tags of at most 40 characters each. | | `owner_id` | string \| null (uuid) | no | The team member who answers for the component; null to leave it unowned. | ### Responses - **200**: The updated component. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The component's id, the value `component_id` carries on an environment. | | `data.project_id` | string | yes | The project the component belongs to. | | `data.name` | string | yes | The component's name, unique within the project. | | `data.slug` | string | yes | The slug a version string names the component by, unique within the project. | | `data.kind` | string | yes | What the component is: `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure` or `other`. | | `data.description` | string \| array of any \| null | yes | What the component does, as HTML filtered to a safe subset; null when none. | | `data.tech_stack` | array of any | yes | The technologies it is built with, as short tags. | | `data.owner_id` | string \| null | yes | The team member who answers for it; null when unowned. | | `data.position` | integer | yes | Its place among the project's components, from zero. | | `data.environments_count` | integer | no | How many environments run the component; present on list responses. | | `data.archived_at` | string \| null | yes | When the component was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the component was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the component last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `component:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, or when the new name or slug is already used by another component of the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Accept a council finding Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/council/accept-a-council-finding `POST /v1/council-findings/{finding}/accept` ```bash curl -X POST https://api.example.test/v1/council-findings/$FINDING_ID/accept \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` The finding joins the review queue as a suggestion carrying the voice; when the voice drafted a record, the record is written through its own door in draft, its first revision stamped as Heimdall's via the council. `council.finding_accepted` goes out, and `import.suggestion_accepted` with it. - Requires ability: `import:update` - Fires events: `council.finding_accepted` - MCP tools: `decide_council_finding` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `finding` | path | string (uuid) | yes | The finding, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The finding, accepted. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The finding's id. | | `data.session_id` | string | yes | The sitting the finding came from. | | `data.turn_id` | string | yes | The voice's turn that left it. | | `data.voice` | string | yes | The voice, by key: `odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`. | | `data.voice_name` | `Heimdall`, `Odin`, `Thor`, `Loki`, `Týr`, `Frigg`, `Baldur`, `Hel` | yes | The voice's name as people read it. | | `data.round` | integer | yes | The round it was left in: 1 for the reading, 2 and up for the exchange and the refinement. | | `data.position` | integer | yes | Its place in the voice's turn, from one, the most serious first. | | `data.title` | string | yes | The finding in a few words. | | `data.claim` | string | yes | What is wrong, missing or at risk, in one or two sentences. | | `data.evidence` | string \| null | yes | The words of the record or of a tool answer the voice rests on; null when it is the voice's judgement. | | `data.suggestion` | string \| null | yes | What to do about it, in the record's own words; null when the voice offered nothing. | | `data.severity` | string | yes | How much it matters: `high`, `medium` or `low`. | | `data.confidence` | number \| null | yes | How sure the voice was, 0 to 1; null when it gave none. | | `data.kind` | string \| null | yes | What accepting it adds to the project: `improvement` for a note, or a record kind such as `business_rule` when the voice drafted the record; null for a note that adds nothing. | | `data.payload` | array \| null | yes | The drafted record's fields, keyed as its editor names them; null when the finding drafts no record. | | `data.status` | string | yes | What was decided: `pending`, `accepted` or `dismissed`. | | `data.suggestion_id` | string \| null | yes | The review queue row the decision wrote; null while pending. | | `data.decided_by` | string \| null | yes | The person who decided; null while pending or when the account is gone. | | `data.decider_name` | string \| null | yes | That person's name, when the decider was loaded; null otherwise. | | `data.decided_at` | string \| null | yes | When it was decided, ISO 8601; null while pending. | | `data.created_at` | string \| null | yes | When the voice left it, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the finding is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the finding was already decided. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Convene the council Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/council/convene-the-council `POST /v1/projects/{project}/council` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/council \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"moment": "requirement_review", "subject_type": "work-item", "subject_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "voices": ["thor", "loki"]}' ``` Writes the session as queued with a turn per voice, puts the voices on the queue as the token's holder and emits `council.convened`. Each voice speaks once (`council.voice_spoke`); when every voice spoke or failed, Heimdall writes the verdict, after one rebuttal round when the voices disagreed (`council.round_opened`), and the session is `ready` (`council.ready`) or `failed` (`council.failed`). Omit `voices` for every voice the project seats on the moment; name voices to summon only some of them, such as one. One sitting per record at a time. Answers `202` with the session, queued, and its turns. - Requires ability: `ai:use` - Fires events: `council.convened` - MCP tools: `convene_council` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) Convene the council on one record: the moment, the record by alias and id, and the voices wanted; omit `voices` for every voice the project seats on the moment. | Field | Type | Required | Description | | --- | --- | --- | --- | | `moment` | `intake_scaffold`, `requirement_review`, `change_impact` | yes | The moment: `intake_scaffold` (a brief import on its review queue) or `requirement_review` (a work item, a requirement or a business rule); the other moments open with their modules. | | `subject_type` | `import-batch`, `project`, `work-item`, `requirement`, `business-rule`, `change-request` | yes | The record's alias: `import-batch` or `project` for the intake scaffold; `work-item`, `requirement` or `business-rule` for the requirement review. | | `subject_id` | string (uuid) | yes | The record's UUID, visible to the token. | | `voices` | array of `odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur`, `hel` | no | The voices to summon, by key, each once; only voices the project seats on the moment sit. Omit for all of them. | ### Responses - **202**: The session, queued, `202`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, the holder's role does not carry `ai:use`, or the project's assists are off. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the project or the record is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the moment is not open, the record does not fit the moment, no voice sits on the moment for the project, the council is already sitting on the record, or the council's share of the month's budget is spent. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Dismiss a council finding Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/council/dismiss-a-council-finding `POST /v1/council-findings/{finding}/dismiss` ```bash curl -X POST https://api.example.test/v1/council-findings/$FINDING_ID/dismiss \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Nothing is written; the idea is remembered on the review queue so no voice and no research run raises it again. `council.finding_dismissed` goes out, and `import.suggestion_rejected` with it. - Requires ability: `import:update` - Fires events: `council.finding_dismissed` - MCP tools: `decide_council_finding` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `finding` | path | string (uuid) | yes | The finding, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The finding, dismissed. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The finding's id. | | `data.session_id` | string | yes | The sitting the finding came from. | | `data.turn_id` | string | yes | The voice's turn that left it. | | `data.voice` | string | yes | The voice, by key: `odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`. | | `data.voice_name` | `Heimdall`, `Odin`, `Thor`, `Loki`, `Týr`, `Frigg`, `Baldur`, `Hel` | yes | The voice's name as people read it. | | `data.round` | integer | yes | The round it was left in: 1 for the reading, 2 and up for the exchange and the refinement. | | `data.position` | integer | yes | Its place in the voice's turn, from one, the most serious first. | | `data.title` | string | yes | The finding in a few words. | | `data.claim` | string | yes | What is wrong, missing or at risk, in one or two sentences. | | `data.evidence` | string \| null | yes | The words of the record or of a tool answer the voice rests on; null when it is the voice's judgement. | | `data.suggestion` | string \| null | yes | What to do about it, in the record's own words; null when the voice offered nothing. | | `data.severity` | string | yes | How much it matters: `high`, `medium` or `low`. | | `data.confidence` | number \| null | yes | How sure the voice was, 0 to 1; null when it gave none. | | `data.kind` | string \| null | yes | What accepting it adds to the project: `improvement` for a note, or a record kind such as `business_rule` when the voice drafted the record; null for a note that adds nothing. | | `data.payload` | array \| null | yes | The drafted record's fields, keyed as its editor names them; null when the finding drafts no record. | | `data.status` | string | yes | What was decided: `pending`, `accepted` or `dismissed`. | | `data.suggestion_id` | string \| null | yes | The review queue row the decision wrote; null while pending. | | `data.decided_by` | string \| null | yes | The person who decided; null while pending or when the account is gone. | | `data.decider_name` | string \| null | yes | That person's name, when the decider was loaded; null otherwise. | | `data.decided_at` | string \| null | yes | When it was decided, ISO 8601; null while pending. | | `data.created_at` | string \| null | yes | When the voice left it, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the finding is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the finding was already decided. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Council API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/council **The council** is Heimdall with named voices beside it: Odin for architecture and the data model, Thor for testing, Loki for security, Týr for rigour and compliance, Frigg for planning and risk, Baldur for the people who will use the product and Hel for what fails in production. A person convenes it on a bounded **moment**, today `intake_scaffold` (a brief import on its review queue, or the project itself once Heimdall scaffolded it in conversation) and `requirement_review` (one work item, requirement or business rule); the other moments open with the change requests, the releases and the incidents they belong to. A **session** is one sitting: each seated voice reads the same brief, the record and the project through its own lens, with the read-only tools the person's role allows, and leaves **findings** (a claim, the evidence it quoted, a suggestion in the record's own words, a severity, a confidence, and when it drafted the missing record itself, the record's kind and fields); when every voice spoke, Heimdall writes the **verdict**: a summary, the findings ranked, the disagreements, and once, in the first round, the voices that should answer each other in a rebuttal round. Nothing reaches a record on a voice's say-so. A person **accepts** a finding (it joins the review queue as a suggestion carrying the voice and, when it drafted a record, the record is written through its own door in draft, attributed to the council) or **dismisses** it (nothing is written and the idea is remembered, so no voice and no research run raises it again). Tokens read the council as staff with `import:view-any` and `import:view`, convene it with `ai:use` on a project whose assists are on, and decide findings with `import:update`; a client guest's token is refused everywhere here. The council spends the team's monthly AI budget within the share the project settings allow it (`council_budget_percent`), and the voices that sit per moment are chosen there too (`council_voices`). ## Endpoints - `GET /v1/projects/{project}/council` — [List a project's council sessions](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/list-a-projects-council-sessions) - `POST /v1/projects/{project}/council` — [Convene the council](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/convene-the-council) - `GET /v1/council/{session}` — [Retrieve a council session](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/retrieve-a-council-session) - `POST /v1/council-findings/{finding}/accept` — [Accept a council finding](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/accept-a-council-finding) - `POST /v1/council-findings/{finding}/dismiss` — [Dismiss a council finding](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/dismiss-a-council-finding) ## Related - [Imports API](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports): The brief imports the intake scaffold sits on, and the review queue accepted findings join. - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items): [requirements](/api/v1/reference/requirements) and [business rules](/api/v1/reference/business-rules): the records the requirement review sits on. - [Project Settings API](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings): The voices per moment and the council's share of the budget. - [council.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council) --- # List a project's council sessions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/council/list-a-projects-council-sessions `GET /v1/projects/{project}/council` ```bash curl https://api.example.test/v1/projects/$PROJECT_ID/council \ -H "Authorization: Bearer $TOKEN" ``` Every sitting on the project, newest first, with its turns: the moment, the record it read, the voices, where it stands, the verdict once written, the counts, the cost, who convened and when. - Requires ability: `import:view-any` - MCP tools: `list_council_sessions`, `list_import_batches`, `list_research_notes` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | ### Responses - **200**: Array of `CouncilSessionResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The session's id. | | `data[].project_id` | string | yes | The project the council sat on. | | `data[].moment` | string | yes | The moment: `intake_scaffold`, `requirement_review`, `change_impact`, `release_readiness` or `post_mortem`. | | `data[].subject_type` | string \| null | yes | The alias of the record the council read: `import-batch` or `project` for the intake scaffold, `work-item`, `requirement` or `business-rule` for the requirement review; null when the moment reads the project at large. | | `data[].subject_id` | string \| null | yes | That record's id; null when none. | | `data[].subject_title` | string \| null | yes | How the record was named when the council sat, kept after the record is gone. | | `data[].status` | string | yes | Where the sitting stands: `queued`, `running`, `summarising`, `ready` or `failed`. | | `data[].voices` | array of any | yes | The voices that sat, by key, in council order: `odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`. | | `data[].round` | integer | yes | The round the sitting is on or ended on: 0 while the voices research, 1 for the reading, 2 and up for the exchange and the refinement. | | `data[].phase` | string | yes | Where the deliberation stands: `research`, `reading`, `discussion`, `planning`, `refining` or `verdict`. | | `data[].summary` | string \| null | yes | Heimdall's verdict in prose; null until the session is ready. | | `data[].plan` | string \| null | yes | The plan as the council agreed it, numbered lines; null until the session is ready. | | `data[].ranked` | array \| null | yes | The findings worth acting on, the most important first; null until the session is ready. | | `data[].ranked[].finding_id` | string | yes | The finding ranked, by id. | | `data[].ranked[].reason` | string | yes | Why it matters, in one sentence of Heimdall's. | | `data[].disagreements` | array \| null | yes | Where two voices pulled apart; null until the session is ready, empty when they agreed. | | `data[].disagreements[].topic` | string | yes | What the voices disagreed about. | | `data[].disagreements[].voices` | array of string | yes | The voices that pulled apart, by key. | | `data[].disagreements[].positions` | string | yes | Each side's position, in a sentence. | | `data[].finding_count` | integer | yes | How many findings the voices left, over every round. | | `data[].cost_micros` | integer | yes | What the sitting cost so far, in millionths of a dollar, summed when it concluded. | | `data[].cost` | string | yes | The same cost as money, such as `$0.0125`. | | `data[].failure_reason` | string \| null | yes | Why the sitting failed, as a person reads it; null otherwise. | | `data[].requested_by` | string \| null | yes | The person who convened; null when the account is gone. | | `data[].requester_name` | string \| null | yes | That person's name, when the requester was loaded; null otherwise. | | `data[].started_at` | string \| null | yes | When the first voice began, ISO 8601; null while queued. | | `data[].finished_at` | string \| null | yes | When the sitting ended, well or badly, ISO 8601; null meanwhile. | | `data[].created_at` | string \| null | yes | When the council was convened, ISO 8601. | | `data[].turns` | array of object | no | The voices' turns, round by round; present when the turns were loaded. | | `data[].turns[].id` | string | yes | The turn's id. | | `data[].turns[].session_id` | string | yes | The sitting the turn belongs to. | | `data[].turns[].voice` | string | yes | The voice, by key: `odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`. | | `data[].turns[].voice_name` | `Heimdall`, `Odin`, `Thor`, `Loki`, `Týr`, `Frigg`, `Baldur`, `Hel` | yes | The voice's name as people read it. | | `data[].turns[].lens` | `Moderator and front door`, `Architecture, design decisions and the data model`, `Testing, verification and defects`, `Security, abuse cases and data exposure`, `Requirement rigour, business rules and compliance`, `Planning, estimates, risks and capacity`, `Experience, accessibility and clarity`, `Incidents, post-mortems and past failures` | yes | The lens the voice reads through, in a few words. | | `data[].turns[].round` | integer | yes | The round the turn belongs to: 0 for the research, 1 for the reading, 2 and up for the exchange and the refinement. | | `data[].turns[].kind` | string | yes | What the voice did in the turn: `research`, `reading`, `reply` or `refinement`. | | `data[].turns[].status` | string | yes | Where the turn stands: `queued`, `running`, `ready` or `failed`. | | `data[].turns[].stance` | string \| null | yes | The voice's words: its take on the record, what it found, its answer to the others or its refinement of the plan; null until it spoke. | | `data[].turns[].sources` | array \| null | yes | What a research turn read, title and web address each; null for any other turn. | | `data[].turns[].finding_count` | integer | yes | How many findings the voice left. | | `data[].turns[].failure_reason` | string \| null | yes | Why the voice failed, as a person reads it; null otherwise. | | `data[].turns[].started_at` | string \| null | yes | When the voice began, ISO 8601; null while queued. | | `data[].turns[].finished_at` | string \| null | yes | When the voice finished, well or badly, ISO 8601; null meanwhile. | | `data[].findings` | array of object | no | Every finding the voices left, in the order they spoke; present when the findings were loaded. | | `data[].findings[].id` | string | yes | The finding's id. | | `data[].findings[].session_id` | string | yes | The sitting the finding came from. | | `data[].findings[].turn_id` | string | yes | The voice's turn that left it. | | `data[].findings[].voice` | string | yes | The voice, by key: `odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`. | | `data[].findings[].voice_name` | `Heimdall`, `Odin`, `Thor`, `Loki`, `Týr`, `Frigg`, `Baldur`, `Hel` | yes | The voice's name as people read it. | | `data[].findings[].round` | integer | yes | The round it was left in: 1 for the reading, 2 and up for the exchange and the refinement. | | `data[].findings[].position` | integer | yes | Its place in the voice's turn, from one, the most serious first. | | `data[].findings[].title` | string | yes | The finding in a few words. | | `data[].findings[].claim` | string | yes | What is wrong, missing or at risk, in one or two sentences. | | `data[].findings[].evidence` | string \| null | yes | The words of the record or of a tool answer the voice rests on; null when it is the voice's judgement. | | `data[].findings[].suggestion` | string \| null | yes | What to do about it, in the record's own words; null when the voice offered nothing. | | `data[].findings[].severity` | string | yes | How much it matters: `high`, `medium` or `low`. | | `data[].findings[].confidence` | number \| null | yes | How sure the voice was, 0 to 1; null when it gave none. | | `data[].findings[].kind` | string \| null | yes | What accepting it adds to the project: `improvement` for a note, or a record kind such as `business_rule` when the voice drafted the record; null for a note that adds nothing. | | `data[].findings[].payload` | array \| null | yes | The drafted record's fields, keyed as its editor names them; null when the finding drafts no record. | | `data[].findings[].status` | string | yes | What was decided: `pending`, `accepted` or `dismissed`. | | `data[].findings[].suggestion_id` | string \| null | yes | The review queue row the decision wrote; null while pending. | | `data[].findings[].decided_by` | string \| null | yes | The person who decided; null while pending or when the account is gone. | | `data[].findings[].decider_name` | string \| null | yes | That person's name, when the decider was loaded; null otherwise. | | `data[].findings[].decided_at` | string \| null | yes | When it was decided, ISO 8601; null while pending. | | `data[].findings[].created_at` | string \| null | yes | When the voice left it, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the project is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Retrieve a council session Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/council/retrieve-a-council-session `GET /v1/council/{session}` ```bash curl https://api.example.test/v1/council/$SESSION_ID \ -H "Authorization: Bearer $TOKEN" ``` One sitting with its turns and every finding. Poll it while the status is `queued`, `running` or `summarising`; read the ranked findings first once it is `ready`. - Requires ability: `import:view` - MCP tools: `get_council_session`, `get_import_batch` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `session` | path | string (uuid) | yes | The session, bound by the middleware. | ### Responses - **200**: The session, with its turns and findings. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the session is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Create a data entity Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/create-a-data-entity `POST /v1/projects/{project}/data-entities` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/data-entities \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Order", "plural": "Orders", "component_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "pii": true, "retention_note": "Kept seven years for tax; personal fields masked after two." }' ``` Only `name` is required; it must be unique within the project, case-insensitively. `component_id`, when sent, names a component of the project and `owner_id` a member of the team. `pii` defaults to false and the status to `draft`. The fields come through the fields endpoint once the entity exists. Answers `201` with the entity, its `fields` empty, and emits `data_entity.created`. - Requires ability: `data-entity:create` - Fires events: `data_entity.created` - MCP tools: `create_data_entity` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A data entity to create or change. Only `name` is required on a create; the status defaults to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves and the fields are replaced through their own endpoints. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | What the entity is called, in the singular, 2 to 80 characters, unique within the project regardless of case. | | `plural` | string \| null | no | The plural, up to 80 characters, when it is not the name with an s. | | `description` | string \| null | no | What the entity is and what it is for, as HTML; filtered to a safe subset when read. | | `component_id` | string \| null (uuid) | no | The component whose records these are, one of the project's; null when the whole project shares them. | | `owner_id` | string \| null (uuid) | no | The team member who answers for the entity; null to leave it unowned. | | `pii` | boolean | no | Whether the entity holds personal data. Defaults to false. | | `retention_note` | string \| null | no | How long records are kept and why, up to 255 characters. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where the entity stands in review, on a create only: `draft`, `in_review`, `approved` or `deprecated`. Defaults to `draft`. | ### Responses - **201**: The entity, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `data-entity:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when `component_id` is not a component of the project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when another entity of the project already uses the name, or when the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a data entity Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/delete-a-data-entity `DELETE /v1/data-entities/{entity}` ```bash curl -X DELETE https://api.example.test/v1/data-entities/$ENTITY_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes an entity with its fields and its activity history; a reference field of another entity that pointed at it loses its target and keeps its name. Returns `204 No Content` and emits `data_entity.deleted`. Requirements and business rules that will name entities are not here yet; once they are, an entity they name will be deprecated rather than deleted. - Requires ability: `data-entity:delete` - Fires events: `data_entity.deleted` - MCP tools: `delete_data_entity` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entity` | path | string (uuid) | yes | The entity, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `data-entity:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a data entity Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/get-a-data-entity `GET /v1/data-entities/{entity}` ```bash curl https://api.example.test/v1/data-entities/$ENTITY_ID \ -H "Authorization: Bearer $TOKEN" ``` One entity with its `fields` in order. An entity of a project the token may not see, or one that is not approved read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `status` is `draft`, `in_review`, `approved` or `deprecated`; `description` is HTML filtered to a safe subset. - Each field's `type` is one of `string`, `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `enum`, `uuid`, `json`, `file` or `reference`; a `reference` field names the entity it points at in `reference_entity_id` and an `enum` field lists its values in `enum_values`. - Requires ability: `data-entity:view` - MCP tools: `get_data_entity` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entity` | path | string (uuid) | yes | The entity, bound by the middleware. | ### Responses - **200**: The entity with its fields. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Data Entities API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities A **data entity** is a thing the project keeps records of: a customer, an order, a branch, a shipment. Each carries its fields with their type, flags, validation and an example, the component whose records they are, the member who answers for it, whether it holds personal data and how long it is kept. Entities and fields are the data dictionary: requirements and business rules will name them, the API contracts and the database designs are read against them, and Heimdall reads them when it drafts stories, rules and test data. The reads here see the entities of the projects the token may see; a client guest's token sees the approved entities only. An entity outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. A listing carries each entity's field count and a single entity carries its fields in order. Every write runs the same action the Data Entities page runs, under the project permissions, and announces itself as a `data_entity.*` event. ## Endpoints - `GET /v1/data-entities/{entity}` — [Get a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/get-a-data-entity) - `PATCH /v1/data-entities/{entity}` — [Update a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/update-a-data-entity) - `DELETE /v1/data-entities/{entity}` — [Delete a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/delete-a-data-entity) - `GET /v1/data-entities/{entity}/revisions` — [List a entity's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-a-entitys-versions) - `GET /v1/data-entities/{entity}/revisions/{revision}` — [Read one version of a entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/read-one-version-of-a-entity) - `GET /v1/projects/{project}/data-entities` — [List data entities](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-data-entities) - `POST /v1/projects/{project}/data-entities` — [Create a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/create-a-data-entity) - `POST /v1/data-entities/{entity}/status` — [Move a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/move-a-data-entity) - `PUT /v1/data-entities/{entity}/fields` — [Replace a data entity's fields](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/replace-a-data-entitys-fields) - `POST /v1/data-entities/{entity}/revisions/{revision}/restore` — [Restore a version of a entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/restore-a-version-of-a-entity) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project an entity belongs to. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components): The component whose records an entity's rows are. - [Glossary API](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary): The other half of the dictionary, the words the project agrees on. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on an entity and who did it. - [data_entity.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity): The lifecycle of a project's data dictionary: an entity created, changed, moved along its review, its fields replaced and the entity deleted. --- # List a entity's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-a-entitys-versions `GET /v1/data-entities/{entity}/revisions` Every version the entity was saved as, newest first, each with who wrote it and why, the columns it changed and the diff from the version before. - Requires ability: `data-entity:view` - MCP tools: `get_data_entity` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entity` | path | string (uuid) | yes | The entity, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List data entities Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-data-entities `GET /v1/projects/{project}/data-entities` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/data-entities?pii=1" \ -H "Authorization: Bearer $TOKEN" ``` The entities of one project in the project's order, each with its `fields_count`, 25 per page (`per_page` 1 to 100). `sort_by` takes `name`, `status`, `position`, `created_at` or `updated_at`. `status` narrows the page to one status, `component_id` to one component's entities, `pii=1` to the entities that hold personal data and `search` to the entities whose name, plural or description contain the words. A client guest's token lists the approved entities whatever `status` asks for. - Requires ability: `data-entity:view-any` - MCP tools: `list_data_entities` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `status` | query | `draft`, `in_review`, `approved`, `deprecated` \| null | no | Narrow to one status: `draft`, `in_review`, `approved` or `deprecated`. An unknown status is refused. | | `component_id` | query | string \| null (uuid) | no | Narrow to the entities of one component. | | `pii` | query | boolean \| null | no | `1` lists the entities that hold personal data only. | | `search` | query | string \| null | no | Words the name, the plural or the description must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of entities, each with its field count. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The entity's id, the value `reference_entity_id` carries on a reference field. | | `data[].project_id` | string | yes | The project the entity belongs to. | | `data[].component_id` | string \| null | yes | The component whose records these are; null when the whole project shares them. | | `data[].name` | string | yes | What the entity is called, in the singular, unique within the project. | | `data[].plural` | string \| null | yes | The plural, when it is not the name with an s; null otherwise. | | `data[].description` | string \| null | yes | What the entity is and what it is for, as HTML filtered to a safe subset; null when none. | | `data[].owner_id` | string \| null | yes | The team member who answers for it; null when unowned. | | `data[].pii` | boolean | yes | Whether the entity holds personal data. | | `data[].retention_note` | string \| null | yes | How long records are kept and why; null when none. | | `data[].status` | string | yes | Where the entity stands in review: `draft`, `in_review`, `approved` or `deprecated`. | | `data[].position` | integer | yes | Its place among the project's entities, from zero. | | `data[].fields_count` | integer | no | How many fields the entity carries; present on list responses. | | `data[].fields` | array of object | no | The fields, in order; present on a single entity. | | `data[].fields[].id` | string | yes | The field's id; send it back in a fields replacement to keep the row. | | `data[].fields[].name` | string | yes | What the field is called, unique within the entity regardless of case. | | `data[].fields[].type` | string | yes | What it holds: `string`, `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `enum`, `uuid`, `json`, `file` or `reference`. | | `data[].fields[].reference_entity_id` | string \| null | yes | The entity a reference field points at; null for any other type. | | `data[].fields[].required` | boolean | yes | Whether a value is required. | | `data[].fields[].unique` | boolean | yes | Whether no two rows share a value. | | `data[].fields[].pii` | boolean | yes | Whether the field is personal data. | | `data[].fields[].validation` | string \| null | yes | The rule a value obeys, in words; null when none. | | `data[].fields[].example` | string \| null | yes | A sample value; null when none. | | `data[].fields[].description` | string \| null | yes | What the field means; null when none. | | `data[].fields[].enum_values` | array of string | yes | The fixed values of an enum field; empty for any other type. | | `data[].fields[].position` | integer | yes | Its place among the entity's fields, from zero. | | `data[].created_at` | string \| null | yes | When the entity was created, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the entity last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is not one of the four statuses, `component_id` is not a UUID, `pii` is not a boolean or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a data entity Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/move-a-data-entity `POST /v1/data-entities/{entity}/status` ```bash curl -X POST https://api.example.test/v1/data-entities/$ENTITY_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "approved"}' ``` Moves the entity along its review: `draft`, `in_review`, `approved` and `deprecated` may each move to any other, except that a deprecated entity can only go back to `draft`. A client guest's token stops seeing an entity the moment it leaves `approved`. Answers `200` with the entity and its fields and emits `data_entity.status_changed` with the status before and after; a move to the status the entity is in already answers `200` and emits nothing. - Requires ability: `data-entity:update` - Fires events: `data_entity.status_changed` - MCP tools: `update_data_entity` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entity` | path | string (uuid) | yes | The entity, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the artefact to. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the artefact goes: `draft`, `in_review`, `approved` or `deprecated`. A deprecated artefact can only move back to `draft`. | ### Responses - **200**: The entity, moved, with its fields. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `data-entity:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, or when the lifecycle refuses the move. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Read one version of a entity Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/read-one-version-of-a-entity `GET /v1/data-entities/{entity}/revisions/{revision}` One version with its full snapshot, the diff from the version before, who wrote it and, for a restore, the version it put back. - Requires ability: `data-entity:view` - MCP tools: `get_data_entity` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entity` | path | string (uuid) | yes | The entity, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version with its snapshot and diff. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Replace a data entity's fields Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/replace-a-data-entitys-fields `PUT /v1/data-entities/{entity}/fields` ```bash curl -X PUT https://api.example.test/v1/data-entities/$ENTITY_ID/fields \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "fields": [ {"id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b", "name": "reference", "type": "string", "required": true, "unique": true, "example": "ORD-000142"}, {"name": "status", "type": "enum", "required": true, "enum_values": ["draft", "placed", "paid", "shipped"]}, {"name": "branch", "type": "reference", "reference_entity_id": "b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d0e", "required": true} ] }' ``` The whole list at once, in the order the fields should keep: a row with an `id` keeps that field and changes it, a row without one adds a field, and a field of the entity missing from the list is removed. Names are unique within the entity regardless of case; an `enum` field lists at least one value; a `reference` field names an entity of the same project. Send an empty list to remove every field. Answers `200` with the entity and its fields as they are now, and emits `data_entity.fields_changed` with how many fields were added, changed and removed; a list that changes nothing answers `200` and emits nothing. - Requires ability: `data-entity:update` - Fires events: `data_entity.fields_changed` - MCP tools: `create_data_entity`, `update_data_entity` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entity` | path | string (uuid) | yes | The entity, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The fields an entity should carry, in order. A row with an `id` keeps that field and changes it; a row without one adds a field; a field of the entity missing from the list is removed. | Field | Type | Required | Description | | --- | --- | --- | --- | | `fields` | array \| null | yes | The fields, in order, up to a hundred. | | `fields[].id` | string \| null (uuid) | no | The id of an existing field to keep; leave it out to add one. | | `fields[].name` | string | yes | What the field is called, up to 80 characters, unique within the entity regardless of case. | | `fields[].type` | `string`, `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `enum`, `uuid`, `json`, `file`, `reference` | no | What it holds: `string`, `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `enum`, `uuid`, `json`, `file` or `reference`. Defaults to `string`. | | `fields[].reference_entity_id` | string \| null (uuid) | no | For a `reference` field, the entity it points at, one of the project's. | | `fields[].required` | boolean | no | Whether a value is required. Defaults to false. | | `fields[].unique` | boolean | no | Whether no two rows share a value. Defaults to false. | | `fields[].pii` | boolean | no | Whether the field is personal data. Defaults to false. | | `fields[].validation` | string \| null | no | The rule a value obeys, in words, up to 255 characters. | | `fields[].example` | string \| null | no | A sample value, up to 255 characters. | | `fields[].description` | string \| null | no | What the field means, up to 2,000 characters. | | `fields[].enum_values` | array \| null | no | For an `enum` field, its fixed values, up to fifty of at most 80 characters each; at least one is required. | ### Responses - **200**: The entity with its fields as they are now. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `data-entity:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a row is malformed, when two rows share a name, when an enum row lists no values, when a reference row names an entity outside the project, or when a row's `id` is not a field of the entity. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a entity Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/restore-a-version-of-a-entity `POST /v1/data-entities/{entity}/revisions/{revision}/restore` Fills the version's snapshot back onto the entity and writes a new version that points at it; nothing earlier is rewritten. - Requires ability: `data-entity:update` - Fires events: `data_entity.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entity` | path | string (uuid) | yes | The entity, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version written, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a data entity Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/update-a-data-entity `PATCH /v1/data-entities/{entity}` ```bash curl -X PATCH https://api.example.test/v1/data-entities/$ENTITY_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"owner_id": "9b2e4f1a-3c5d-4e6f-8a7b-1c2d3e4f5a6b", "retention_note": null}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. A new name must be free within the project. The status moves and the fields are replaced through their own endpoints. Answers `200` with the entity and its fields and emits `data_entity.updated` with the columns that changed, each with its old and new value; an update that changes nothing answers `200` and emits nothing. - Requires ability: `data-entity:update` - Fires events: `data_entity.updated` - MCP tools: `update_data_entity` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entity` | path | string (uuid) | yes | The entity, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A data entity to create or change. Only `name` is required on a create; the status defaults to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves and the fields are replaced through their own endpoints. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | What the entity is called, in the singular, 2 to 80 characters, unique within the project regardless of case. | | `plural` | string \| null | no | The plural, up to 80 characters, when it is not the name with an s. | | `description` | string \| null | no | What the entity is and what it is for, as HTML; filtered to a safe subset when read. | | `component_id` | string \| null (uuid) | no | The component whose records these are, one of the project's; null when the whole project shares them. | | `owner_id` | string \| null (uuid) | no | The team member who answers for the entity; null to leave it unowned. | | `pii` | boolean | no | Whether the entity holds personal data. Defaults to false. | | `retention_note` | string \| null | no | How long records are kept and why, up to 255 characters. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where the entity stands in review, on a create only: `draft`, `in_review`, `approved` or `deprecated`. Defaults to `draft`. | ### Responses - **200**: The entity with its fields. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `data-entity:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when `component_id` is not a component of the project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when the new name is already used by another entity of the project, or when the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a decision Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/add-a-decision `POST /v1/projects/{project}/decisions` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/decisions \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "title": "Keep the loyalty ledger in Postgres", "kind": "architecture", "context": "

The ledger must be queried with the orders and audited.

", "options": [ { "title": "A ledger table in the main Postgres database", "pros": "One backup, one transaction", "cons": "Needs partitioning within two years" }, { "title": "A dedicated event store", "pros": "Append-only by design", "cons": "A second system to run" } ], "decision": "

The ledger lives in Postgres, partitioned by month.

" }' ``` Only `title` is required. The decision takes the next number of the decisions' sequence and the reference spelled from the project's key, starts `proposed` unless `status` says otherwise and as an `architecture` decision unless `kind` does. Every decider must be a member of the team, and a superseded decision must be another decision of the same project. The save writes version one of the decision. Answers `201` with the decision and emits `decision.created`. - Requires ability: `decision:create` - Fires events: `decision.created` - MCP tools: `record_decision_draft` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A decision to add or change. Only `title` is required on a create; the kind defaults to `architecture` and the status to `proposed`. On an update, a field left out keeps its value and a field sent as null is cleared; `options` and `deciders`, when sent, replace their lists; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the decision is called, 3 to 200 characters. | | `kind` | `architecture`, `product`, `process`, `vendor`, `security` | no | The kind: `architecture`, `product`, `process`, `vendor` or `security`. Defaults to `architecture`. | | `context` | string \| null | no | The situation and the forces at play, as HTML; filtered to a safe subset when read. | | `options` | array \| null | no | The options weighed, up to ten rows of `title` (required), `pros` and `cons`. Sent, they replace the list; left out, the list stays. | | `options[].title` | string | yes | What the option is, 2 to 200 characters; required. | | `options[].pros` | string \| null | no | What speaks for it, up to 1,000 characters. | | `options[].cons` | string \| null | no | What speaks against it, up to 1,000 characters. | | `decision` | string \| null | no | What was decided, as HTML. | | `consequences` | string \| null | no | What follows, as HTML. | | `deciders` | array \| null | no | The people who decided, up to twenty user ids, each a member of the team. Sent, they replace the list; left out, the list stays. | | `supersedes_id` | string \| null (uuid) | no | The id of the earlier decision this one replaces, a decision of the same project; null for none. | | `status` | `proposed`, `accepted`, `rejected`, `superseded`, `deprecated` | no | Where it starts, on a create only: `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`. Defaults to `proposed`. | ### Responses - **201**: The decision, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The decision's id. | | `data.project_id` | string | yes | The project the decision belongs to. | | `data.number` | integer | yes | Its number in the decisions' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-DEC-5`. | | `data.kind` | string | yes | The kind: `architecture`, `product`, `process`, `vendor` or `security`. | | `data.title` | string | yes | What the decision is called. | | `data.context` | string \| null | yes | The situation and the forces at play, as HTML filtered to a safe subset; null when none. | | `data.options` | array of object | yes | The options weighed in order, each `{ "title": …, "pros": …, "cons": … }`; empty when none. | | `data.options[].title` | string | yes | What the option is. | | `data.options[].pros` | string \| null | yes | What speaks for it; null when not written. | | `data.options[].cons` | string \| null | yes | What speaks against it; null when not written. | | `data.decision` | string \| null | yes | What was decided, as HTML; null while only proposed. | | `data.consequences` | string \| null | yes | What follows from the decision, as HTML; null when none. | | `data.status` | string | yes | Where it stands: `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`. | | `data.decided_on` | string \| null | yes | The day it was accepted, `Y-m-d`; null until then. | | `data.deciders` | array of string | yes | The people who decided, by user id; empty when none were named. | | `data.supersedes_id` | string \| null | yes | The earlier decision this one replaces; null when none. | | `data.supersedes_reference` | string \| null | yes | That decision's reference, when it was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's decisions, from zero. | | `data.created_at` | string \| null | yes | When the decision was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the decision last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `decision:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, an option lacks its title, a decider is not a member of the team, or the superseded decision is not one of the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a decision Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/delete-a-decision `DELETE /v1/decisions/{decision}` ```bash curl -X DELETE https://api.example.test/v1/decisions/$DECISION_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a decision with its versions and its activity history; a later decision that named it keeps its own record and loses the link. The number it took is never handed out again. Returns `204 No Content` and emits `decision.deleted`. Prefer a move to `deprecated` for a decision that no longer applies; it keeps the reasoning. - Requires ability: `decision:delete` - Fires events: `decision.deleted` - MCP tools: `delete_decision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `decision` | path | string (uuid) | yes | The decision, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `decision:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a decision Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-decision `GET /v1/decisions/{decision}` ```bash curl https://api.example.test/v1/decisions/$DECISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One decision with the reference of the one it supersedes. A decision of a project the token may not see, or any decision read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `kind` is `architecture`, `product`, `process`, `vendor` or `security`; `status` is `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`; `options` are rows of `title`, `pros` and `cons`; `deciders` are user ids. - Requires ability: `decision:view` - MCP tools: `get_decision` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `decision` | path | string (uuid) | yes | The decision, bound by the middleware. | ### Responses - **200**: The decision. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The decision's id. | | `data.project_id` | string | yes | The project the decision belongs to. | | `data.number` | integer | yes | Its number in the decisions' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-DEC-5`. | | `data.kind` | string | yes | The kind: `architecture`, `product`, `process`, `vendor` or `security`. | | `data.title` | string | yes | What the decision is called. | | `data.context` | string \| null | yes | The situation and the forces at play, as HTML filtered to a safe subset; null when none. | | `data.options` | array of object | yes | The options weighed in order, each `{ "title": …, "pros": …, "cons": … }`; empty when none. | | `data.options[].title` | string | yes | What the option is. | | `data.options[].pros` | string \| null | yes | What speaks for it; null when not written. | | `data.options[].cons` | string \| null | yes | What speaks against it; null when not written. | | `data.decision` | string \| null | yes | What was decided, as HTML; null while only proposed. | | `data.consequences` | string \| null | yes | What follows from the decision, as HTML; null when none. | | `data.status` | string | yes | Where it stands: `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`. | | `data.decided_on` | string \| null | yes | The day it was accepted, `Y-m-d`; null until then. | | `data.deciders` | array of string | yes | The people who decided, by user id; empty when none were named. | | `data.supersedes_id` | string \| null | yes | The earlier decision this one replaces; null when none. | | `data.supersedes_reference` | string \| null | yes | That decision's reference, when it was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's decisions, from zero. | | `data.created_at` | string \| null | yes | When the decision was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the decision last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a version of a decision Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-version-of-a-decision `GET /v1/decisions/{decision}/revisions/{revision}` ```bash curl https://api.example.test/v1/decisions/$DECISION_ID/revisions/$REVISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One version of the decision. A version of another artefact, a version the token may not see or a client guest's token is `404 RESOURCE_NOT_FOUND`. - Requires ability: `decision:view` - MCP tools: `get_decision` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `decision` | path | string (uuid) | yes | The decision, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Decisions API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions A **decision** is the record of a choice the project made: the context it was taken in, the options weighed with their pros and cons, what was decided, what follows, who decided and when, and the earlier decision it replaces. Decisions number on their own sequence (`CRM-DEC-5`), carry a kind (architecture, product, process, vendor or security) and move from proposed to accepted or rejected, and from accepted to superseded or deprecated. Accepting a decision that names an earlier one supersedes that one in the same move, so the log never holds two decisions in force on the same question. Every save of a decision writes a **version**; the versions are read and put back through the endpoints below, under the decisions' own abilities. The log is internal. A staff token sees the decisions of the projects it may see; a client guest's token sees none of them, and a decision it asks for is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Decisions page runs, under the project permissions, and announces itself as a `decision.*` event. ## Endpoints - `GET /v1/decisions/{decision}` — [Get a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-decision) - `PATCH /v1/decisions/{decision}` — [Update a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/update-a-decision) - `DELETE /v1/decisions/{decision}` — [Delete a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/delete-a-decision) - `GET /v1/decisions/{decision}/revisions` — [List a decision's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-a-decisions-versions) - `GET /v1/decisions/{decision}/revisions/{revision}` — [Get a version of a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-version-of-a-decision) - `GET /v1/projects/{project}/decisions` — [List decisions](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-decisions) - `POST /v1/projects/{project}/decisions` — [Add a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/add-a-decision) - `POST /v1/decisions/{decision}/status` — [Move a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/move-a-decision) - `POST /v1/decisions/{decision}/revisions/{revision}/restore` — [Restore a version of a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/restore-a-version-of-a-decision) ## Related - [RAID Log API](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log): The risks and dependencies a decision answers to. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): Who changed a decision and when, beside the versions themselves. - [decision.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision): The lifecycle of a project's decisions: a decision added, changed, moved along its lifecycle, put back to an earlier version and deleted. --- # List a decision's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-a-decisions-versions `GET /v1/decisions/{decision}/revisions` ```bash curl "https://api.example.test/v1/decisions/$DECISION_ID/revisions" \ -H "Authorization: Bearer $TOKEN" ``` Every version of the decision, newest first, 25 per page (`per_page` 1 to 100). Each carries its `version` number, the `reason` it was written, who wrote it (`author_id`, `author_name`) and through what (`actor_kind`), a one-line `summary`, the `changed_fields`, the `diff` from the version before and the full `snapshot`. A client guest's token is refused. - Requires ability: `decision:view` - MCP tools: `get_decision` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `decision` | path | string (uuid) | yes | The decision, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role does not carry `decision:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List decisions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-decisions `GET /v1/projects/{project}/decisions` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/decisions?status=accepted" \ -H "Authorization: Bearer $TOKEN" ``` The decisions of one project in the project's order, 25 per page (`per_page` 1 to 100), each with the reference of the decision it supersedes. `sort_by` takes `number`, `reference`, `title`, `kind`, `status`, `decided_on`, `position`, `created_at` or `updated_at`. `kind` and `status` narrow the page to one value each, and `search` to the decisions whose reference or title contain the words. - Requires ability: `decision:view-any` - MCP tools: `list_decisions` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `kind` | query | `architecture`, `product`, `process`, `vendor`, `security` \| null | no | Narrow to one kind: `architecture`, `product`, `process`, `vendor` or `security`. An unknown kind is refused. | | `status` | query | `proposed`, `accepted`, `rejected`, `superseded`, `deprecated` \| null | no | Narrow to one status: `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`. | | `search` | query | string \| null | no | Words the reference or the title must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of decisions. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The decision's id. | | `data[].project_id` | string | yes | The project the decision belongs to. | | `data[].number` | integer | yes | Its number in the decisions' own sequence. | | `data[].reference` | string | yes | The key people use, such as `CRM-DEC-5`. | | `data[].kind` | string | yes | The kind: `architecture`, `product`, `process`, `vendor` or `security`. | | `data[].title` | string | yes | What the decision is called. | | `data[].context` | string \| null | yes | The situation and the forces at play, as HTML filtered to a safe subset; null when none. | | `data[].options` | array of object | yes | The options weighed in order, each `{ "title": …, "pros": …, "cons": … }`; empty when none. | | `data[].options[].title` | string | yes | What the option is. | | `data[].options[].pros` | string \| null | yes | What speaks for it; null when not written. | | `data[].options[].cons` | string \| null | yes | What speaks against it; null when not written. | | `data[].decision` | string \| null | yes | What was decided, as HTML; null while only proposed. | | `data[].consequences` | string \| null | yes | What follows from the decision, as HTML; null when none. | | `data[].status` | string | yes | Where it stands: `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`. | | `data[].decided_on` | string \| null | yes | The day it was accepted, `Y-m-d`; null until then. | | `data[].deciders` | array of string | yes | The people who decided, by user id; empty when none were named. | | `data[].supersedes_id` | string \| null | yes | The earlier decision this one replaces; null when none. | | `data[].supersedes_reference` | string \| null | yes | That decision's reference, when it was loaded; null otherwise. | | `data[].position` | integer | yes | Its place among the project's decisions, from zero. | | `data[].created_at` | string \| null | yes | When the decision was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the decision last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `decision:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `kind` or `status` is not one of its values, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a decision Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/move-a-decision `POST /v1/decisions/{decision}/status` ```bash curl -X POST https://api.example.test/v1/decisions/$DECISION_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "accepted"}' ``` Moves the decision along its lifecycle: a proposed decision is accepted or rejected, an accepted one superseded or deprecated, a rejected or deprecated one proposed again, a superseded one accepted again. A move into `accepted` stamps the day it was decided (when none was set) and, when the decision names an earlier one in `supersedes_id`, moves that one to `superseded` with its own event. The move writes a version with the reason `transition`. Answers `200` with the decision and emits `decision.status_changed` with the status before and after; a move to the status the decision is in already answers `200` and emits nothing. - Requires ability: `decision:update` - Fires events: `decision.status_changed` - MCP tools: `update_decision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `decision` | path | string (uuid) | yes | The decision, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the decision to. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `proposed`, `accepted`, `rejected`, `superseded`, `deprecated` | yes | Where the decision goes: `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`. A proposed decision is accepted or rejected; an accepted one is superseded or deprecated; a rejected or deprecated one is proposed again; a superseded one is accepted again. | ### Responses - **200**: The decision, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The decision's id. | | `data.project_id` | string | yes | The project the decision belongs to. | | `data.number` | integer | yes | Its number in the decisions' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-DEC-5`. | | `data.kind` | string | yes | The kind: `architecture`, `product`, `process`, `vendor` or `security`. | | `data.title` | string | yes | What the decision is called. | | `data.context` | string \| null | yes | The situation and the forces at play, as HTML filtered to a safe subset; null when none. | | `data.options` | array of object | yes | The options weighed in order, each `{ "title": …, "pros": …, "cons": … }`; empty when none. | | `data.options[].title` | string | yes | What the option is. | | `data.options[].pros` | string \| null | yes | What speaks for it; null when not written. | | `data.options[].cons` | string \| null | yes | What speaks against it; null when not written. | | `data.decision` | string \| null | yes | What was decided, as HTML; null while only proposed. | | `data.consequences` | string \| null | yes | What follows from the decision, as HTML; null when none. | | `data.status` | string | yes | Where it stands: `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`. | | `data.decided_on` | string \| null | yes | The day it was accepted, `Y-m-d`; null until then. | | `data.deciders` | array of string | yes | The people who decided, by user id; empty when none were named. | | `data.supersedes_id` | string \| null | yes | The earlier decision this one replaces; null when none. | | `data.supersedes_reference` | string \| null | yes | That decision's reference, when it was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's decisions, from zero. | | `data.created_at` | string \| null | yes | When the decision was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the decision last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `decision:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, or when the lifecycle refuses the move. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a decision Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/restore-a-version-of-a-decision `POST /v1/decisions/{decision}/revisions/{revision}/restore` ```bash curl -X POST https://api.example.test/v1/decisions/$DECISION_ID/revisions/$REVISION_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the version's captured columns back onto the decision, status included, and saves. The save writes a new version with the reason `restore` that names the one put back in `restored_from_id`; nothing earlier is rewritten. Answers `201` with the version written and emits `decision.restored` with both version numbers. - Requires ability: `decision:update` - Fires events: `decision.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `decision` | path | string (uuid) | yes | The decision, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version the restore wrote, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `decision:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the version is not the decision's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the version is identical to the current record, so there is nothing to restore. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a decision Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/update-a-decision `PATCH /v1/decisions/{decision}` ```bash curl -X PATCH https://api.example.test/v1/decisions/$DECISION_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"consequences": "

The reporting queries join the ledger directly.

", "deciders": ["c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f"]}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. `options` and `deciders`, when sent, replace their lists in full. The status does not change here; the move endpoint owns it. A save that changes a captured column writes a new version. Answers `200` with the decision and emits `decision.updated` with the columns that changed, each with its old and new value; an update that changes no column answers `200` and emits nothing. - Requires ability: `decision:update` - Fires events: `decision.updated` - MCP tools: `update_decision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `decision` | path | string (uuid) | yes | The decision, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A decision to add or change. Only `title` is required on a create; the kind defaults to `architecture` and the status to `proposed`. On an update, a field left out keeps its value and a field sent as null is cleared; `options` and `deciders`, when sent, replace their lists; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the decision is called, 3 to 200 characters. | | `kind` | `architecture`, `product`, `process`, `vendor`, `security` | no | The kind: `architecture`, `product`, `process`, `vendor` or `security`. Defaults to `architecture`. | | `context` | string \| null | no | The situation and the forces at play, as HTML; filtered to a safe subset when read. | | `options` | array \| null | no | The options weighed, up to ten rows of `title` (required), `pros` and `cons`. Sent, they replace the list; left out, the list stays. | | `options[].title` | string | yes | What the option is, 2 to 200 characters; required. | | `options[].pros` | string \| null | no | What speaks for it, up to 1,000 characters. | | `options[].cons` | string \| null | no | What speaks against it, up to 1,000 characters. | | `decision` | string \| null | no | What was decided, as HTML. | | `consequences` | string \| null | no | What follows, as HTML. | | `deciders` | array \| null | no | The people who decided, up to twenty user ids, each a member of the team. Sent, they replace the list; left out, the list stays. | | `supersedes_id` | string \| null (uuid) | no | The id of the earlier decision this one replaces, a decision of the same project; null for none. | | `status` | `proposed`, `accepted`, `rejected`, `superseded`, `deprecated` | no | Where it starts, on a create only: `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`. Defaults to `proposed`. | ### Responses - **200**: The decision. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The decision's id. | | `data.project_id` | string | yes | The project the decision belongs to. | | `data.number` | integer | yes | Its number in the decisions' own sequence. | | `data.reference` | string | yes | The key people use, such as `CRM-DEC-5`. | | `data.kind` | string | yes | The kind: `architecture`, `product`, `process`, `vendor` or `security`. | | `data.title` | string | yes | What the decision is called. | | `data.context` | string \| null | yes | The situation and the forces at play, as HTML filtered to a safe subset; null when none. | | `data.options` | array of object | yes | The options weighed in order, each `{ "title": …, "pros": …, "cons": … }`; empty when none. | | `data.options[].title` | string | yes | What the option is. | | `data.options[].pros` | string \| null | yes | What speaks for it; null when not written. | | `data.options[].cons` | string \| null | yes | What speaks against it; null when not written. | | `data.decision` | string \| null | yes | What was decided, as HTML; null while only proposed. | | `data.consequences` | string \| null | yes | What follows from the decision, as HTML; null when none. | | `data.status` | string | yes | Where it stands: `proposed`, `accepted`, `rejected`, `superseded` or `deprecated`. | | `data.decided_on` | string \| null | yes | The day it was accepted, `Y-m-d`; null until then. | | `data.deciders` | array of string | yes | The people who decided, by user id; empty when none were named. | | `data.supersedes_id` | string \| null | yes | The earlier decision this one replaces; null when none. | | `data.supersedes_reference` | string \| null | yes | That decision's reference, when it was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's decisions, from zero. | | `data.created_at` | string \| null | yes | When the decision was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the decision last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `decision:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, a decider is not a member of the team, or the decision would supersede itself or one of another project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a design reference Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/add-a-design-reference `POST /v1/projects/{project}/design-references` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/design-references \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "title": "Checkout flow", "url": "https://www.figma.com/file/aB3dE5fG7hJ9kL1mN3pQ5r/Checkout", "kind": "figma", "referenceable_type": "component", "referenceable_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "version": "v3" }' ``` `title` and `url` are required; the address must be `http` or `https`. `kind` defaults to `other`. The reference hangs off the whole project unless `referenceable_type` is `component` and `referenceable_id` names one of the project's components. The token holder is remembered as `added_by`. Answers `201` with the reference and emits `design_reference.created`. - Requires ability: `design-reference:create` - Fires events: `design_reference.created` - MCP tools: `create_design_reference` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A design reference to add or change. Only `title` and `url` are required on a create; the kind defaults to `other` and the reference hangs off the whole project unless a component is named. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the reference is called, 2 to 120 characters. | | `url` | string (uri) | no | Where the design lives, an `http` or `https` address of up to 2,048 characters. | | `kind` | `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document`, `other` | no | What the link points at: `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document` or `other`. Defaults to `other`. | | `referenceable_type` | `project`, `component` | no | What to attach the reference to: `project` for the whole project or `component` for one of its components. Defaults to `project`. | | `referenceable_id` | string \| null (uuid) | no | The component's id when `referenceable_type` is `component`; ignored for the project. | | `version` | string \| null | no | The version or revision of the design, up to 40 characters. | | `notes` | string \| null | no | What to look at and why, as HTML; filtered to a safe subset when read. | ### Responses - **201**: The reference, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The reference's id. | | `data.project_id` | string | yes | The project the reference belongs to. | | `data.referenceable_type` | string | yes | What the reference is attached to: `project` for the whole project or `component` for one component. | | `data.referenceable_id` | string | yes | The id of the project or the component it is attached to. | | `data.kind` | string | yes | What the link points at: `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document` or `other`. | | `data.title` | string | yes | What the reference is called. | | `data.url` | string | yes | Where the design lives, an `http` or `https` address. | | `data.version` | string \| null | yes | The version or revision of the design, as the designer labels it; null when none. | | `data.notes` | string \| null | yes | What to look at and why, as HTML filtered to a safe subset; null when none. | | `data.added_by` | string \| null | yes | The user who added the reference; null when that account is gone. | | `data.position` | integer | yes | Its place among the project's references, from zero. | | `data.created_at` | string \| null | yes | When the reference was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the reference last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `design-reference:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when `referenceable_id` is not a component of the project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when the address is not an `http` or `https` link, or when `referenceable_type` is `component` and `referenceable_id` is missing. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a design reference Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/delete-a-design-reference `DELETE /v1/design-references/{reference}` ```bash curl -X DELETE https://api.example.test/v1/design-references/$REFERENCE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a reference with its activity history; the design itself stays where the designer keeps it. Returns `204 No Content` and emits `design_reference.deleted`. - Requires ability: `design-reference:delete` - Fires events: `design_reference.deleted` - MCP tools: `delete_design_reference` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `reference` | path | string (uuid) | yes | The reference, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `design-reference:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a design reference Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/get-a-design-reference `GET /v1/design-references/{reference}` ```bash curl https://api.example.test/v1/design-references/$REFERENCE_ID \ -H "Authorization: Bearer $TOKEN" ``` One reference. A reference of a project the token may not see is `404 RESOURCE_NOT_FOUND`. - `kind` is `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document` or `other`. - `referenceable_type` is `project` or `component` and `referenceable_id` the id of that project or component. - `notes` is HTML filtered to a safe subset. - Requires ability: `design-reference:view` - MCP tools: `get_design_reference` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `reference` | path | string (uuid) | yes | The reference, bound by the middleware. | ### Responses - **200**: The reference. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The reference's id. | | `data.project_id` | string | yes | The project the reference belongs to. | | `data.referenceable_type` | string | yes | What the reference is attached to: `project` for the whole project or `component` for one component. | | `data.referenceable_id` | string | yes | The id of the project or the component it is attached to. | | `data.kind` | string | yes | What the link points at: `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document` or `other`. | | `data.title` | string | yes | What the reference is called. | | `data.url` | string | yes | Where the design lives, an `http` or `https` address. | | `data.version` | string \| null | yes | The version or revision of the design, as the designer labels it; null when none. | | `data.notes` | string \| null | yes | What to look at and why, as HTML filtered to a safe subset; null when none. | | `data.added_by` | string \| null | yes | The user who added the reference; null when that account is gone. | | `data.position` | integer | yes | Its place among the project's references, from zero. | | `data.created_at` | string \| null | yes | When the reference was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the reference last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Design References API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references A **design reference** is a link a project keeps to one of its designs: a Figma file, a wireframe, a prototype, a screenshot, an API contract, a sequence diagram, an entity diagram or a document. The link is the record and the file stays where the designer keeps it; the reference says what it is, which version it is, what to look at and whether it belongs to the whole project or to one component. Developers and testers read the references before they build or check a screen, and the client signs off against them. The reads here see the references of the projects the token may see, a client guest's token among them, because the designs are what the client reviews. A reference outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Designs page runs, under the project permissions, and announces itself as a `design_reference.*` event. ## Endpoints - `GET /v1/design-references/{reference}` — [Get a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/get-a-design-reference) - `PATCH /v1/design-references/{reference}` — [Update a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/update-a-design-reference) - `DELETE /v1/design-references/{reference}` — [Delete a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/delete-a-design-reference) - `GET /v1/projects/{project}/design-references` — [List design references](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/list-design-references) - `POST /v1/projects/{project}/design-references` — [Add a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/add-a-design-reference) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project a reference belongs to. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components): The component a reference may be attached to. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on a reference and who did it. - [design_reference.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference): The lifecycle of a project's design references: a link added, changed and removed. --- # List design references Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/list-design-references `GET /v1/projects/{project}/design-references` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/design-references?kind=figma" \ -H "Authorization: Bearer $TOKEN" ``` The references of one project in the project's order, 25 per page (`per_page` 1 to 100). `sort_by` takes `title`, `kind`, `position`, `created_at` or `updated_at`. `kind` narrows the page to one kind, `component_id` to the references attached to one component, `project_wide=1` to the references attached to the whole project and `search` to the references whose title, address or version contain the words. - Requires ability: `design-reference:view-any` - MCP tools: `list_design_references` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `kind` | query | `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document`, `other` \| null | no | Narrow to one kind: `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document` or `other`. An unknown kind is refused. | | `component_id` | query | string \| null (uuid) | no | Narrow to the references attached to one component. | | `project_wide` | query | boolean \| null | no | `1` lists the references attached to the whole project only. | | `search` | query | string \| null | no | Words the title, the address or the version must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of references. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The reference's id. | | `data[].project_id` | string | yes | The project the reference belongs to. | | `data[].referenceable_type` | string | yes | What the reference is attached to: `project` for the whole project or `component` for one component. | | `data[].referenceable_id` | string | yes | The id of the project or the component it is attached to. | | `data[].kind` | string | yes | What the link points at: `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document` or `other`. | | `data[].title` | string | yes | What the reference is called. | | `data[].url` | string | yes | Where the design lives, an `http` or `https` address. | | `data[].version` | string \| null | yes | The version or revision of the design, as the designer labels it; null when none. | | `data[].notes` | string \| null | yes | What to look at and why, as HTML filtered to a safe subset; null when none. | | `data[].added_by` | string \| null | yes | The user who added the reference; null when that account is gone. | | `data[].position` | integer | yes | Its place among the project's references, from zero. | | `data[].created_at` | string \| null | yes | When the reference was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the reference last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `kind` is not one of its values, `component_id` is not a UUID, `project_wide` is not a boolean or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a design reference Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/update-a-design-reference `PATCH /v1/design-references/{reference}` ```bash curl -X PATCH https://api.example.test/v1/design-references/$REFERENCE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"version": "v4", "notes": "

Frames 12 to 20 are the signed-off flow.

"}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. To move a reference onto a component send `referenceable_type` and `referenceable_id` together; to move it back onto the whole project send `referenceable_type` as `project`. Answers `200` with the reference and emits `design_reference.updated` with the columns that changed, each with its old and new value; an update that changes nothing answers `200` and emits nothing. - Requires ability: `design-reference:update` - Fires events: `design_reference.updated` - MCP tools: `update_design_reference` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `reference` | path | string (uuid) | yes | The reference, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A design reference to add or change. Only `title` and `url` are required on a create; the kind defaults to `other` and the reference hangs off the whole project unless a component is named. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the reference is called, 2 to 120 characters. | | `url` | string (uri) | no | Where the design lives, an `http` or `https` address of up to 2,048 characters. | | `kind` | `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document`, `other` | no | What the link points at: `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document` or `other`. Defaults to `other`. | | `referenceable_type` | `project`, `component` | no | What to attach the reference to: `project` for the whole project or `component` for one of its components. Defaults to `project`. | | `referenceable_id` | string \| null (uuid) | no | The component's id when `referenceable_type` is `component`; ignored for the project. | | `version` | string \| null | no | The version or revision of the design, up to 40 characters. | | `notes` | string \| null | no | What to look at and why, as HTML; filtered to a safe subset when read. | ### Responses - **200**: The reference. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The reference's id. | | `data.project_id` | string | yes | The project the reference belongs to. | | `data.referenceable_type` | string | yes | What the reference is attached to: `project` for the whole project or `component` for one component. | | `data.referenceable_id` | string | yes | The id of the project or the component it is attached to. | | `data.kind` | string | yes | What the link points at: `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document` or `other`. | | `data.title` | string | yes | What the reference is called. | | `data.url` | string | yes | Where the design lives, an `http` or `https` address. | | `data.version` | string \| null | yes | The version or revision of the design, as the designer labels it; null when none. | | `data.notes` | string \| null | yes | What to look at and why, as HTML filtered to a safe subset; null when none. | | `data.added_by` | string \| null | yes | The user who added the reference; null when that account is gone. | | `data.position` | integer | yes | Its place among the project's references, from zero. | | `data.created_at` | string \| null | yes | When the reference was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the reference last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `design-reference:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when `referenceable_id` is not a component of the project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when the address is not an `http` or `https` link, or when `referenceable_type` is `component` and no component is named. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Create an environment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/create-an-environment `POST /v1/projects/{project}/environments` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/environments \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Staging", "kind": "staging", "component_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "url": "https://staging.example.test", "branch": "develop", "health_check_url": "https://staging.example.test/up" }' ``` Only `name` is required; it must be unique within the project and component, case-insensitively. `kind` defaults to `development`. The component, when named, must be one of the project's. A `health_check_url` switches the scheduled check on for the environment. Answers `201` with the environment and emits `environment.created`. - Requires ability: `environment:create` - Fires events: `environment.created` - MCP tools: `create_environment` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) An environment to create or change. Only `name` is required on a create. On an update, a field left out keeps its value and a field sent as null is cleared. The health and the freeze have their own endpoints. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The environment's name, 2 to 60 characters, unique within the project and component. | | `kind` | `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom` | no | What it is for: `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` or `custom`. Defaults to `development` on a create. | | `component_id` | string \| null (uuid) | no | The component it runs, one of the project's; null when it serves the whole project. | | `url` | string \| null (uri) | no | The address people open, as a full URL. | | `api_url` | string \| null (uri) | no | The address the API answers on, as a full URL. | | `admin_url` | string \| null (uri) | no | The address of the back office, as a full URL. | | `branch` | string \| null | no | The branch deployed there, up to 120 characters. | | `hosting` | string \| null | no | Where it runs, in words, up to 120 characters. | | `region` | string \| null | no | The region it runs in, up to 60 characters. | | `auto_deploy` | boolean | no | Whether the pipeline deploys to it without a hand. Defaults to false. | | `protected` | boolean | no | Whether a deployment into it needs a promotion approval. Defaults to false. | | `health_check_url` | string \| null (uri) | no | The address the scheduled check calls every few minutes, as a full URL; null switches the check off. | | `current_version` | string \| null | no | The version running there, as last recorded, up to 60 characters. | | `notes` | string \| null | no | What the team should know about the environment, as HTML; filtered to a safe subset when read. | ### Responses - **201**: The environment as a 201. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The environment's id, the value `environment_id` carries on a deployment. | | `data.project_id` | string | yes | The project the environment belongs to. | | `data.component_id` | string \| null | yes | The component it runs; null when it serves the whole project. | | `data.name` | string | yes | The environment's name, unique within the project and component. | | `data.kind` | string | yes | What it is for: `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` or `custom`. | | `data.url` | string \| null | yes | The address people open; null when none. | | `data.api_url` | string \| null | yes | The address the API answers on; null when none. | | `data.admin_url` | string \| null | no | The address of the back office; null when none. Left out for a guest. | | `data.branch` | string \| null | no | The branch deployed there; null when none. Left out for a guest. | | `data.hosting` | string \| null | no | Where it runs, in words; null when none. Left out for a guest. | | `data.region` | string \| null | no | The region it runs in; null when none. Left out for a guest. | | `data.auto_deploy` | boolean | yes | Whether the pipeline deploys to it without a hand. | | `data.protected` | boolean | yes | Whether a deployment into it needs a promotion approval. | | `data.health` | string | yes | What the last check found: `healthy`, `degraded`, `down` or `unknown` when never checked. | | `data.health_check_url` | string \| null | no | The address the scheduled check calls; null when the check is off. Left out for a guest. | | `data.last_checked_at` | string \| null | yes | When the health was last recorded, ISO 8601; null when never. | | `data.consecutive_failures` | integer | no | How many checks in a row have found the environment down. Left out for a guest. | | `data.current_version` | string \| null | yes | The version running there, as last recorded; null when unknown. | | `data.frozen_until` | string \| null | yes | When the change freeze ends, ISO 8601; null when not frozen. | | `data.frozen` | boolean | yes | Whether a change freeze is in force right now. | | `data.notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data.position` | integer | yes | Its place among the project's environments, from zero. | | `data.created_at` | string \| null | yes | When the environment was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the environment last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `environment:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when `component_id` is not a component of the project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, or when another environment of the same project and component already uses the name. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete an environment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/delete-an-environment `DELETE /v1/environments/{environment}` ```bash curl -X DELETE https://api.example.test/v1/environments/$ENVIRONMENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes an environment with its history. Returns `204 No Content` and emits `environment.deleted`. Once deployments land, an environment with deployment history will be archived rather than deleted. - Requires ability: `environment:delete` - Fires events: `environment.deleted` - MCP tools: `delete_environment` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `environment` | path | string (uuid) | yes | The environment, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `environment:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Freeze an environment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/freeze-an-environment `POST /v1/environments/{environment}/freeze` ```bash curl -X POST https://api.example.test/v1/environments/$ENVIRONMENT_ID/freeze \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"frozen_until": "2026-12-31T23:59:00+00:00"}' ``` Starts a change freeze: until `frozen_until`, deployments into the environment are refused. A freeze already in force is replaced by the new end. Answers `200` with the environment, `frozen` true, and emits `environment.frozen`. - Requires ability: `environment:freeze` - Fires events: `environment.frozen` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `environment` | path | string (uuid) | yes | The environment, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) When the freeze ends. Until then deployments into the environment are refused. | Field | Type | Required | Description | | --- | --- | --- | --- | | `frozen_until` | string (date-time) | yes | When the freeze ends, ISO 8601, in the future. | ### Responses - **200**: The environment with its freeze. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The environment's id, the value `environment_id` carries on a deployment. | | `data.project_id` | string | yes | The project the environment belongs to. | | `data.component_id` | string \| null | yes | The component it runs; null when it serves the whole project. | | `data.name` | string | yes | The environment's name, unique within the project and component. | | `data.kind` | string | yes | What it is for: `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` or `custom`. | | `data.url` | string \| null | yes | The address people open; null when none. | | `data.api_url` | string \| null | yes | The address the API answers on; null when none. | | `data.admin_url` | string \| null | no | The address of the back office; null when none. Left out for a guest. | | `data.branch` | string \| null | no | The branch deployed there; null when none. Left out for a guest. | | `data.hosting` | string \| null | no | Where it runs, in words; null when none. Left out for a guest. | | `data.region` | string \| null | no | The region it runs in; null when none. Left out for a guest. | | `data.auto_deploy` | boolean | yes | Whether the pipeline deploys to it without a hand. | | `data.protected` | boolean | yes | Whether a deployment into it needs a promotion approval. | | `data.health` | string | yes | What the last check found: `healthy`, `degraded`, `down` or `unknown` when never checked. | | `data.health_check_url` | string \| null | no | The address the scheduled check calls; null when the check is off. Left out for a guest. | | `data.last_checked_at` | string \| null | yes | When the health was last recorded, ISO 8601; null when never. | | `data.consecutive_failures` | integer | no | How many checks in a row have found the environment down. Left out for a guest. | | `data.current_version` | string \| null | yes | The version running there, as last recorded; null when unknown. | | `data.frozen_until` | string \| null | yes | When the change freeze ends, ISO 8601; null when not frozen. | | `data.frozen` | boolean | yes | Whether a change freeze is in force right now. | | `data.notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data.position` | integer | yes | Its place among the project's environments, from zero. | | `data.created_at` | string \| null | yes | When the environment was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the environment last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `environment:freeze`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `frozen_until` is missing, malformed or not in the future. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get an environment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/get-an-environment `GET /v1/environments/{environment}` ```bash curl https://api.example.test/v1/environments/$ENVIRONMENT_ID \ -H "Authorization: Bearer $TOKEN" ``` One environment. An environment of a project the token may not see, or one a guest token may not read, is `404 RESOURCE_NOT_FOUND`. - `kind` runs `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` and `custom`. - `health` is `healthy`, `degraded`, `down` or `unknown` when never checked; `last_checked_at` says when it was last written, by the scheduled check or by a report over the API. - `frozen` is true while `frozen_until` is in the future. - Requires ability: `environment:view` - MCP tools: `get_environment` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `environment` | path | string (uuid) | yes | The environment, resolved by the route binder. | ### Responses - **200**: The environment. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The environment's id, the value `environment_id` carries on a deployment. | | `data.project_id` | string | yes | The project the environment belongs to. | | `data.component_id` | string \| null | yes | The component it runs; null when it serves the whole project. | | `data.name` | string | yes | The environment's name, unique within the project and component. | | `data.kind` | string | yes | What it is for: `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` or `custom`. | | `data.url` | string \| null | yes | The address people open; null when none. | | `data.api_url` | string \| null | yes | The address the API answers on; null when none. | | `data.admin_url` | string \| null | no | The address of the back office; null when none. Left out for a guest. | | `data.branch` | string \| null | no | The branch deployed there; null when none. Left out for a guest. | | `data.hosting` | string \| null | no | Where it runs, in words; null when none. Left out for a guest. | | `data.region` | string \| null | no | The region it runs in; null when none. Left out for a guest. | | `data.auto_deploy` | boolean | yes | Whether the pipeline deploys to it without a hand. | | `data.protected` | boolean | yes | Whether a deployment into it needs a promotion approval. | | `data.health` | string | yes | What the last check found: `healthy`, `degraded`, `down` or `unknown` when never checked. | | `data.health_check_url` | string \| null | no | The address the scheduled check calls; null when the check is off. Left out for a guest. | | `data.last_checked_at` | string \| null | yes | When the health was last recorded, ISO 8601; null when never. | | `data.consecutive_failures` | integer | no | How many checks in a row have found the environment down. Left out for a guest. | | `data.current_version` | string \| null | yes | The version running there, as last recorded; null when unknown. | | `data.frozen_until` | string \| null | yes | When the change freeze ends, ISO 8601; null when not frozen. | | `data.frozen` | boolean | yes | Whether a change freeze is in force right now. | | `data.notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data.position` | integer | yes | Its place among the project's environments, from zero. | | `data.created_at` | string \| null | yes | When the environment was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the environment last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Environments API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/environments An **environment** is one named place a project's software runs, from a developer's machine to production: its kind, the component it runs (or the whole project), its addresses, the branch deployed there, the `auto_deploy` and `protected` flags, the health the last check found, the version running there and whether a change freeze is in force. Deployments will land here and releases will promote through the kinds in order; a protected environment will need a promotion approval, and a frozen one refuses deployments until the freeze lifts. The reads here see the environments of the projects the token may see. A client guest's token reads only the UAT and production environments, without the admin address, the branch, the hosting note, the region, the check's address and streak, and the notes, which are left out of the payload rather than nulled. An environment outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Environments page runs, under the project permissions, and announces itself as an `environment.*` event. No endpoint here carries a secret: the addresses are what a browser or a pipeline opens. ## Endpoints - `GET /v1/environments/{environment}` — [Get an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/get-an-environment) - `PATCH /v1/environments/{environment}` — [Update an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/update-an-environment) - `DELETE /v1/environments/{environment}` — [Delete an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/delete-an-environment) - `GET /v1/projects/{project}/environments` — [List environments](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/list-environments) - `POST /v1/projects/{project}/environments` — [Create an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/create-an-environment) - `POST /v1/environments/{environment}/freeze` — [Freeze an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/freeze-an-environment) - `POST /v1/environments/{environment}/unfreeze` — [Unfreeze an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/unfreeze-an-environment) - `POST /v1/environments/{environment}/health` — [Record an environment's health](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/record-an-environments-health) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project an environment belongs to. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components): The application an environment runs. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on an environment and who did it. - [environment.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment): The lifecycle and the state of a project's environments: creation, edits, deletion, change freezes and the health the checks find. --- # List environments Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/list-environments `GET /v1/projects/{project}/environments` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/environments?kind=production" \ -H "Authorization: Bearer $TOKEN" ``` The environments of one project in the project's order, 25 per page (`per_page` 1 to 100). `sort_by` takes `name`, `kind`, `health`, `position`, `last_checked_at`, `created_at` or `updated_at`. `kind`, `component_id`, `health` and `search` narrow the page. A guest token lists the UAT and production environments only. - Requires ability: `environment:view-any` - MCP tools: `list_environments` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, resolved by the route binder. | | `kind` | query | `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom` \| null | no | Narrow to one kind, such as `staging` or `production`. An unknown kind is refused. | | `component_id` | query | string \| null (uuid) | no | Narrow to the environments of one component. | | `health` | query | `healthy`, `degraded`, `down`, `unknown` \| null | no | Narrow to one health: `healthy`, `degraded`, `down` or `unknown`. | | `search` | query | string \| null | no | Words the name, the address or the hosting note must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: The page, in the project's order. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The environment's id, the value `environment_id` carries on a deployment. | | `data[].project_id` | string | yes | The project the environment belongs to. | | `data[].component_id` | string \| null | yes | The component it runs; null when it serves the whole project. | | `data[].name` | string | yes | The environment's name, unique within the project and component. | | `data[].kind` | string | yes | What it is for: `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` or `custom`. | | `data[].url` | string \| null | yes | The address people open; null when none. | | `data[].api_url` | string \| null | yes | The address the API answers on; null when none. | | `data[].admin_url` | string \| null | no | The address of the back office; null when none. Left out for a guest. | | `data[].branch` | string \| null | no | The branch deployed there; null when none. Left out for a guest. | | `data[].hosting` | string \| null | no | Where it runs, in words; null when none. Left out for a guest. | | `data[].region` | string \| null | no | The region it runs in; null when none. Left out for a guest. | | `data[].auto_deploy` | boolean | yes | Whether the pipeline deploys to it without a hand. | | `data[].protected` | boolean | yes | Whether a deployment into it needs a promotion approval. | | `data[].health` | string | yes | What the last check found: `healthy`, `degraded`, `down` or `unknown` when never checked. | | `data[].health_check_url` | string \| null | no | The address the scheduled check calls; null when the check is off. Left out for a guest. | | `data[].last_checked_at` | string \| null | yes | When the health was last recorded, ISO 8601; null when never. | | `data[].consecutive_failures` | integer | no | How many checks in a row have found the environment down. Left out for a guest. | | `data[].current_version` | string \| null | yes | The version running there, as last recorded; null when unknown. | | `data[].frozen_until` | string \| null | yes | When the change freeze ends, ISO 8601; null when not frozen. | | `data[].frozen` | boolean | yes | Whether a change freeze is in force right now. | | `data[].notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data[].position` | integer | yes | Its place among the project's environments, from zero. | | `data[].created_at` | string \| null | yes | When the environment was created, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the environment last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `kind` or `health` is not one of its values, `component_id` is not a UUID, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Record an environment's health Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/record-an-environments-health `POST /v1/environments/{environment}/health` ```bash curl -X POST https://api.example.test/v1/environments/$ENVIRONMENT_ID/health \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"health": "down"}' ``` Records what a monitoring system or a person observed, as checked now, beside what the scheduled check writes. A report of `down` lengthens the count of checks in a row that found the environment down; any other health resets it. Answers `200` with the environment and emits `environment.health_changed` when the health changed; a report that confirms what was known answers `200` and emits nothing. - Requires ability: `environment:update` - Fires events: `environment.health_changed` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `environment` | path | string (uuid) | yes | The environment, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The health a monitoring system or a person observed. Recorded as checked now; a change of state is announced as `environment.health_changed`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `health` | `healthy`, `degraded`, `down`, `unknown` | yes | What was observed: `healthy`, `degraded`, `down` or `unknown`. | ### Responses - **200**: The environment with its health and check time. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The environment's id, the value `environment_id` carries on a deployment. | | `data.project_id` | string | yes | The project the environment belongs to. | | `data.component_id` | string \| null | yes | The component it runs; null when it serves the whole project. | | `data.name` | string | yes | The environment's name, unique within the project and component. | | `data.kind` | string | yes | What it is for: `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` or `custom`. | | `data.url` | string \| null | yes | The address people open; null when none. | | `data.api_url` | string \| null | yes | The address the API answers on; null when none. | | `data.admin_url` | string \| null | no | The address of the back office; null when none. Left out for a guest. | | `data.branch` | string \| null | no | The branch deployed there; null when none. Left out for a guest. | | `data.hosting` | string \| null | no | Where it runs, in words; null when none. Left out for a guest. | | `data.region` | string \| null | no | The region it runs in; null when none. Left out for a guest. | | `data.auto_deploy` | boolean | yes | Whether the pipeline deploys to it without a hand. | | `data.protected` | boolean | yes | Whether a deployment into it needs a promotion approval. | | `data.health` | string | yes | What the last check found: `healthy`, `degraded`, `down` or `unknown` when never checked. | | `data.health_check_url` | string \| null | no | The address the scheduled check calls; null when the check is off. Left out for a guest. | | `data.last_checked_at` | string \| null | yes | When the health was last recorded, ISO 8601; null when never. | | `data.consecutive_failures` | integer | no | How many checks in a row have found the environment down. Left out for a guest. | | `data.current_version` | string \| null | yes | The version running there, as last recorded; null when unknown. | | `data.frozen_until` | string \| null | yes | When the change freeze ends, ISO 8601; null when not frozen. | | `data.frozen` | boolean | yes | Whether a change freeze is in force right now. | | `data.notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data.position` | integer | yes | Its place among the project's environments, from zero. | | `data.created_at` | string \| null | yes | When the environment was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the environment last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `environment:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `health` is missing or not one of `healthy`, `degraded`, `down` and `unknown`. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Unfreeze an environment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/unfreeze-an-environment `POST /v1/environments/{environment}/unfreeze` ```bash curl -X POST https://api.example.test/v1/environments/$ENVIRONMENT_ID/unfreeze \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Lifts a change freeze before it ends. Answers `200` with the environment, `frozen` false, and emits `environment.unfrozen`. - Requires ability: `environment:freeze` - Fires events: `environment.unfrozen` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `environment` | path | string (uuid) | yes | The environment, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The environment, open to deployments again. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The environment's id, the value `environment_id` carries on a deployment. | | `data.project_id` | string | yes | The project the environment belongs to. | | `data.component_id` | string \| null | yes | The component it runs; null when it serves the whole project. | | `data.name` | string | yes | The environment's name, unique within the project and component. | | `data.kind` | string | yes | What it is for: `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` or `custom`. | | `data.url` | string \| null | yes | The address people open; null when none. | | `data.api_url` | string \| null | yes | The address the API answers on; null when none. | | `data.admin_url` | string \| null | no | The address of the back office; null when none. Left out for a guest. | | `data.branch` | string \| null | no | The branch deployed there; null when none. Left out for a guest. | | `data.hosting` | string \| null | no | Where it runs, in words; null when none. Left out for a guest. | | `data.region` | string \| null | no | The region it runs in; null when none. Left out for a guest. | | `data.auto_deploy` | boolean | yes | Whether the pipeline deploys to it without a hand. | | `data.protected` | boolean | yes | Whether a deployment into it needs a promotion approval. | | `data.health` | string | yes | What the last check found: `healthy`, `degraded`, `down` or `unknown` when never checked. | | `data.health_check_url` | string \| null | no | The address the scheduled check calls; null when the check is off. Left out for a guest. | | `data.last_checked_at` | string \| null | yes | When the health was last recorded, ISO 8601; null when never. | | `data.consecutive_failures` | integer | no | How many checks in a row have found the environment down. Left out for a guest. | | `data.current_version` | string \| null | yes | The version running there, as last recorded; null when unknown. | | `data.frozen_until` | string \| null | yes | When the change freeze ends, ISO 8601; null when not frozen. | | `data.frozen` | boolean | yes | Whether a change freeze is in force right now. | | `data.notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data.position` | integer | yes | Its place among the project's environments, from zero. | | `data.created_at` | string \| null | yes | When the environment was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the environment last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `environment:freeze`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the environment is not frozen. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update an environment Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/update-an-environment `PATCH /v1/environments/{environment}` ```bash curl -X PATCH https://api.example.test/v1/environments/$ENVIRONMENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"branch": "release/1.4", "current_version": "1.4.0-rc.2"}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. The health and the freeze are not fields here; each has its own endpoint below. Answers `200` with the environment and emits `environment.updated` with the columns that changed, each with its old and new value; an update that changes nothing answers `200` and emits nothing. - Requires ability: `environment:update` - Fires events: `environment.updated` - MCP tools: `update_environment` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `environment` | path | string (uuid) | yes | The environment, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) An environment to create or change. Only `name` is required on a create. On an update, a field left out keeps its value and a field sent as null is cleared. The health and the freeze have their own endpoints. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The environment's name, 2 to 60 characters, unique within the project and component. | | `kind` | `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom` | no | What it is for: `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` or `custom`. Defaults to `development` on a create. | | `component_id` | string \| null (uuid) | no | The component it runs, one of the project's; null when it serves the whole project. | | `url` | string \| null (uri) | no | The address people open, as a full URL. | | `api_url` | string \| null (uri) | no | The address the API answers on, as a full URL. | | `admin_url` | string \| null (uri) | no | The address of the back office, as a full URL. | | `branch` | string \| null | no | The branch deployed there, up to 120 characters. | | `hosting` | string \| null | no | Where it runs, in words, up to 120 characters. | | `region` | string \| null | no | The region it runs in, up to 60 characters. | | `auto_deploy` | boolean | no | Whether the pipeline deploys to it without a hand. Defaults to false. | | `protected` | boolean | no | Whether a deployment into it needs a promotion approval. Defaults to false. | | `health_check_url` | string \| null (uri) | no | The address the scheduled check calls every few minutes, as a full URL; null switches the check off. | | `current_version` | string \| null | no | The version running there, as last recorded, up to 60 characters. | | `notes` | string \| null | no | What the team should know about the environment, as HTML; filtered to a safe subset when read. | ### Responses - **200**: The updated environment. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The environment's id, the value `environment_id` carries on a deployment. | | `data.project_id` | string | yes | The project the environment belongs to. | | `data.component_id` | string \| null | yes | The component it runs; null when it serves the whole project. | | `data.name` | string | yes | The environment's name, unique within the project and component. | | `data.kind` | string | yes | What it is for: `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production` or `custom`. | | `data.url` | string \| null | yes | The address people open; null when none. | | `data.api_url` | string \| null | yes | The address the API answers on; null when none. | | `data.admin_url` | string \| null | no | The address of the back office; null when none. Left out for a guest. | | `data.branch` | string \| null | no | The branch deployed there; null when none. Left out for a guest. | | `data.hosting` | string \| null | no | Where it runs, in words; null when none. Left out for a guest. | | `data.region` | string \| null | no | The region it runs in; null when none. Left out for a guest. | | `data.auto_deploy` | boolean | yes | Whether the pipeline deploys to it without a hand. | | `data.protected` | boolean | yes | Whether a deployment into it needs a promotion approval. | | `data.health` | string | yes | What the last check found: `healthy`, `degraded`, `down` or `unknown` when never checked. | | `data.health_check_url` | string \| null | no | The address the scheduled check calls; null when the check is off. Left out for a guest. | | `data.last_checked_at` | string \| null | yes | When the health was last recorded, ISO 8601; null when never. | | `data.consecutive_failures` | integer | no | How many checks in a row have found the environment down. Left out for a guest. | | `data.current_version` | string \| null | yes | The version running there, as last recorded; null when unknown. | | `data.frozen_until` | string \| null | yes | When the change freeze ends, ISO 8601; null when not frozen. | | `data.frozen` | boolean | yes | Whether a change freeze is in force right now. | | `data.notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data.position` | integer | yes | Its place among the project's environments, from zero. | | `data.created_at` | string \| null | yes | When the environment was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the environment last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `environment:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when `component_id` is not a component of the project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, or when the new name is already used by another environment of the same project and component. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add an epic Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/add-an-epic `POST /v1/projects/{project}/epics` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/epics \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "title": "Loyalty tiers", "goal": "Reward the top fifth of members so they keep buying from the client.", "priority": "must", "due_on": "2027-03-31" }' ``` Only `title` is required. The epic takes the next number of the project's tracker sequence and the reference spelled from the project's key, starts as `draft` unless `status` says otherwise, and `should` unless `priority` does. `owner_id` must be a member of the team. The save writes version one of the epic. Answers `201` with the epic and emits `epic.created`. - Requires ability: `epic:create` - Fires events: `epic.created` - MCP tools: `create_epic` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) An epic to add or change. Only `title` is required on a create; the priority defaults to `should` and the status to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the epic is called, 3 to 160 characters. | | `summary` | string \| null | no | What it covers, as HTML; filtered to a safe subset when read. | | `goal` | string \| null | no | One sentence on what it achieves, up to 300 characters. | | `business_value` | string \| null | no | Why the client wants it, as HTML. | | `hypothesis` | string \| null | no | The bet it makes, up to 500 characters. | | `success_metrics` | string \| null | no | How the team will know it worked, as HTML. | | `owner_id` | string \| null (uuid) | no | The id of the person who answers for it, a member of the team; null for nobody. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority: `must`, `should`, `could` or `wont`. Defaults to `should`. | | `status` | `draft`, `in_review`, `approved`, `in_progress`, `done`, `cancelled` | no | Where it starts, on a create only: `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`. Defaults to `draft`. | | `starts_on` | string \| null (date) | no | When work on it starts, `Y-m-d`. | | `due_on` | string \| null (date) | no | When it is due, `Y-m-d`. | | `color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`, null | no | The badge colour the board and the roadmap paint it in, one of the configured colour names; null for the default. | ### Responses - **201**: The epic, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The epic's id. | | `data.project_id` | string | yes | The project the epic belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key stories, commits and people use, such as `CRM-12`. | | `data.title` | string | yes | What the epic is called. | | `data.summary` | string \| null | yes | What it covers, as HTML filtered to a safe subset; null when none. | | `data.goal` | string \| null | yes | One sentence on what it achieves; null when none. | | `data.business_value` | string \| null | yes | Why the client wants it, as HTML; null when none. | | `data.hypothesis` | string \| null | yes | The bet it makes; null when none. | | `data.success_metrics` | string \| null | yes | How the team will know it worked, as HTML; null when none. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`. | | `data.starts_on` | string \| null | yes | When work on it starts, `Y-m-d`; null when not agreed. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.color` | string \| null | yes | The badge colour the board and the roadmap paint it in; null for the default. | | `data.position` | integer | yes | Its place among the project's epics, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the epic was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the epic last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `epic:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when a date is not `Y-m-d`, when `color` is not a configured colour, or when the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete an epic Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/delete-an-epic `DELETE /v1/epics/{epic}` ```bash curl -X DELETE https://api.example.test/v1/epics/$EPIC_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes an epic with its versions and its activity history. The number it took is never handed out again. Returns `204 No Content` and emits `epic.deleted`. The work items under it stay in the backlog and lose their epic. Prefer a move to `cancelled`; it keeps the record and the items attached. - Requires ability: `epic:delete` - Fires events: `epic.deleted` - MCP tools: `delete_epic` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `epic` | path | string (uuid) | yes | The epic, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `epic:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a version of an epic Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-a-version-of-an-epic `GET /v1/epics/{epic}/revisions/{revision}` ```bash curl https://api.example.test/v1/epics/$EPIC_ID/revisions/$REVISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One version of the epic. A version of another artefact, a version the token may not see or a client guest's token is `404 RESOURCE_NOT_FOUND`. - `reason` is `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`; `actor_kind` is `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. - `diff` is null for version one; `restored_from_id` and `restored_from_version` are set for a version a restore wrote. - Requires ability: `epic:view` - MCP tools: `get_revision` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `epic` | path | string (uuid) | yes | The epic, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get an epic Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-an-epic `GET /v1/epics/{epic}` ```bash curl https://api.example.test/v1/epics/$EPIC_ID \ -H "Authorization: Bearer $TOKEN" ``` One epic with its owner's name. An epic of a project the token may not see, or an epic that is not agreed read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `status` is `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`; `priority` is `must`, `should`, `could` or `wont`. - `summary`, `business_value` and `success_metrics` are HTML filtered to a safe subset. - `version` counts approvals and `approved_revision_id` names the version the last one froze; both stay at zero and null until the approvals land. - Requires ability: `epic:view` - MCP tools: `get_epic` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `epic` | path | string (uuid) | yes | The epic, bound by the middleware. | ### Responses - **200**: The epic. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The epic's id. | | `data.project_id` | string | yes | The project the epic belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key stories, commits and people use, such as `CRM-12`. | | `data.title` | string | yes | What the epic is called. | | `data.summary` | string \| null | yes | What it covers, as HTML filtered to a safe subset; null when none. | | `data.goal` | string \| null | yes | One sentence on what it achieves; null when none. | | `data.business_value` | string \| null | yes | Why the client wants it, as HTML; null when none. | | `data.hypothesis` | string \| null | yes | The bet it makes; null when none. | | `data.success_metrics` | string \| null | yes | How the team will know it worked, as HTML; null when none. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`. | | `data.starts_on` | string \| null | yes | When work on it starts, `Y-m-d`; null when not agreed. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.color` | string \| null | yes | The badge colour the board and the roadmap paint it in; null for the default. | | `data.position` | integer | yes | Its place among the project's epics, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the epic was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the epic last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Epics API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics An **epic** is a body of work the client recognises: the scope a release is cut from and the parent of every story. It is numbered from the project's shared tracker sequence, so its reference (`CRM-12`) sits in the same series as the stories under it, and it carries the goal, the value and the measures that say when it is done, an owner, a MoSCoW priority, a status from draft to done and the dates agreed. Every save of an epic writes a **version**: the snapshot of its captured columns, the diff from the version before, who wrote it, through what, and why. The versions are read and put back through the endpoints below, under the epic's own abilities; a restore writes a new version that points at the one restored, so the history is never rewritten. The reads here see the epics of the projects the token may see; a client guest's token sees the agreed epics only (approved, in progress or done), because a draft is still the analyst's working note, and never a version. An epic outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Epics page runs, under the project permissions, and announces itself as an `epic.*` event. ## Endpoints - `GET /v1/epics/{epic}` — [Get an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-an-epic) - `PATCH /v1/epics/{epic}` — [Update an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/update-an-epic) - `DELETE /v1/epics/{epic}` — [Delete an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/delete-an-epic) - `GET /v1/epics/{epic}/revisions` — [List an epic's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-an-epics-versions) - `GET /v1/epics/{epic}/revisions/{revision}` — [Get a version of an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-a-version-of-an-epic) - `GET /v1/projects/{project}/epics` — [List epics](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-epics) - `POST /v1/projects/{project}/epics` — [Add an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/add-an-epic) - `POST /v1/epics/{epic}/status` — [Move an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/move-an-epic) - `POST /v1/epics/{epic}/revisions/{revision}/restore` — [Restore a version of an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/restore-a-version-of-an-epic) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project an epic belongs to. - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas): The people the stories under an epic are written as. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): Who changed an epic and when, beside the versions themselves. - [epic.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic): The lifecycle of a project's epics: an epic added, changed, moved along its lifecycle, put back to an earlier version and deleted. --- # List an epic's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-an-epics-versions `GET /v1/epics/{epic}/revisions` ```bash curl "https://api.example.test/v1/epics/$EPIC_ID/revisions" \ -H "Authorization: Bearer $TOKEN" ``` Every version of the epic, newest first, 25 per page (`per_page` 1 to 100). Each carries its `version` number, the `reason` it was written, who wrote it (`author_id`, `author_name`) and through what (`actor_kind`), a one-line `summary`, the `changed_fields`, the `diff` from the version before and the full `snapshot`. A client guest's token is refused: the versions are the drafts behind the agreed record. - Requires ability: `epic:view` - MCP tools: `list_revisions` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `epic` | path | string (uuid) | yes | The epic, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role does not carry `epic:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List epics Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-epics `GET /v1/projects/{project}/epics` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/epics?status=approved&priority=must" \ -H "Authorization: Bearer $TOKEN" ``` The epics of one project in the project's order, 25 per page (`per_page` 1 to 100). `sort_by` takes `number`, `reference`, `title`, `status`, `priority`, `starts_on`, `due_on`, `position`, `created_at` or `updated_at`. `status` narrows the page to one status, `priority` to one MoSCoW priority, `owner_id` to one owner and `search` to the epics whose reference, title or goal contain the words. A client guest's token lists the agreed epics whatever `status` asks for. - Requires ability: `epic:view-any` - MCP tools: `list_epics` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `status` | query | `draft`, `in_review`, `approved`, `in_progress`, `done`, `cancelled` \| null | no | Narrow to one status: `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`. An unknown status is refused. | | `priority` | query | `must`, `should`, `could`, `wont` \| null | no | Narrow to one MoSCoW priority: `must`, `should`, `could` or `wont`. | | `owner_id` | query | string \| null (uuid) | no | Narrow to the epics one person answers for. | | `search` | query | string \| null | no | Words the reference, the title or the goal must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of epics. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The epic's id. | | `data[].project_id` | string | yes | The project the epic belongs to. | | `data[].number` | integer | yes | Its number in the project's shared tracker sequence. | | `data[].reference` | string | yes | The key stories, commits and people use, such as `CRM-12`. | | `data[].title` | string | yes | What the epic is called. | | `data[].summary` | string \| null | yes | What it covers, as HTML filtered to a safe subset; null when none. | | `data[].goal` | string \| null | yes | One sentence on what it achieves; null when none. | | `data[].business_value` | string \| null | yes | Why the client wants it, as HTML; null when none. | | `data[].hypothesis` | string \| null | yes | The bet it makes; null when none. | | `data[].success_metrics` | string \| null | yes | How the team will know it worked, as HTML; null when none. | | `data[].owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data[].owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data[].priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data[].status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`. | | `data[].starts_on` | string \| null | yes | When work on it starts, `Y-m-d`; null when not agreed. | | `data[].due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data[].color` | string \| null | yes | The badge colour the board and the roadmap paint it in; null for the default. | | `data[].position` | integer | yes | Its place among the project's epics, from zero. | | `data[].version` | integer | yes | How many times it was approved; zero until the first approval. | | `data[].approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data[].approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data[].created_at` | string \| null | yes | When the epic was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the epic last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` or `priority` is not one of its values, `owner_id` is not a uuid, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move an epic Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/move-an-epic `POST /v1/epics/{epic}/status` ```bash curl -X POST https://api.example.test/v1/epics/$EPIC_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "in_review"}' ``` Moves the epic along its lifecycle. A draft goes to `in_review` or `cancelled`; a review goes back to `draft`, on to `approved` or to `cancelled`; an approved epic goes back to `in_review`, on to `in_progress` or to `cancelled`; one in progress goes to `done` or `cancelled`; a done epic may be reopened to `in_progress`; a cancelled one is revived as a `draft`. A move into `approved` stamps `approved_at`. A client guest's token sees the epic from the moment it is approved and loses it when it is cancelled. The move writes a version of the epic with the reason `transition`. Answers `200` with the epic and emits `epic.status_changed` with the status before and after; a move to the status the epic is in already answers `200` and emits nothing. - Requires ability: `epic:update` - Fires events: `epic.status_changed` - MCP tools: `update_epic` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `epic` | path | string (uuid) | yes | The epic, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the epic to. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft`, `in_review`, `approved`, `in_progress`, `done`, `cancelled` | yes | Where the epic goes: `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`. The move must be one the lifecycle allows from the current status. | ### Responses - **200**: The epic, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The epic's id. | | `data.project_id` | string | yes | The project the epic belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key stories, commits and people use, such as `CRM-12`. | | `data.title` | string | yes | What the epic is called. | | `data.summary` | string \| null | yes | What it covers, as HTML filtered to a safe subset; null when none. | | `data.goal` | string \| null | yes | One sentence on what it achieves; null when none. | | `data.business_value` | string \| null | yes | Why the client wants it, as HTML; null when none. | | `data.hypothesis` | string \| null | yes | The bet it makes; null when none. | | `data.success_metrics` | string \| null | yes | How the team will know it worked, as HTML; null when none. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`. | | `data.starts_on` | string \| null | yes | When work on it starts, `Y-m-d`; null when not agreed. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.color` | string \| null | yes | The badge colour the board and the roadmap paint it in; null for the default. | | `data.position` | integer | yes | Its place among the project's epics, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the epic was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the epic last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `epic:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, or when the lifecycle refuses the move. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of an epic Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/restore-a-version-of-an-epic `POST /v1/epics/{epic}/revisions/{revision}/restore` ```bash curl -X POST https://api.example.test/v1/epics/$EPIC_ID/revisions/$REVISION_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the version's captured columns back onto the epic, status included, and saves. The save writes a new version with the reason `restore` that names the one put back in `restored_from_id`; nothing earlier is rewritten. Answers `201` with the version written and emits `epic.restored` with both version numbers. - Requires ability: `epic:update` - Fires events: `epic.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `epic` | path | string (uuid) | yes | The epic, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version the restore wrote, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `epic:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the version is not the epic's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the version is identical to the current record, so there is nothing to restore. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update an epic Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/update-an-epic `PATCH /v1/epics/{epic}` ```bash curl -X PATCH https://api.example.test/v1/epics/$EPIC_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"priority": "must", "due_on": "2027-03-31", "hypothesis": null}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. The status does not change here; the move endpoint owns it. A save that changes one of the captured columns writes a new version of the epic. Answers `200` with the epic and emits `epic.updated` with the columns that changed, each with its old and new value; an update that changes nothing answers `200` and emits nothing. - Requires ability: `epic:update` - Fires events: `epic.updated` - MCP tools: `update_epic` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `epic` | path | string (uuid) | yes | The epic, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) An epic to add or change. Only `title` is required on a create; the priority defaults to `should` and the status to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the epic is called, 3 to 160 characters. | | `summary` | string \| null | no | What it covers, as HTML; filtered to a safe subset when read. | | `goal` | string \| null | no | One sentence on what it achieves, up to 300 characters. | | `business_value` | string \| null | no | Why the client wants it, as HTML. | | `hypothesis` | string \| null | no | The bet it makes, up to 500 characters. | | `success_metrics` | string \| null | no | How the team will know it worked, as HTML. | | `owner_id` | string \| null (uuid) | no | The id of the person who answers for it, a member of the team; null for nobody. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority: `must`, `should`, `could` or `wont`. Defaults to `should`. | | `status` | `draft`, `in_review`, `approved`, `in_progress`, `done`, `cancelled` | no | Where it starts, on a create only: `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`. Defaults to `draft`. | | `starts_on` | string \| null (date) | no | When work on it starts, `Y-m-d`. | | `due_on` | string \| null (date) | no | When it is due, `Y-m-d`. | | `color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`, null | no | The badge colour the board and the roadmap paint it in, one of the configured colour names; null for the default. | ### Responses - **200**: The epic. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The epic's id. | | `data.project_id` | string | yes | The project the epic belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key stories, commits and people use, such as `CRM-12`. | | `data.title` | string | yes | What the epic is called. | | `data.summary` | string \| null | yes | What it covers, as HTML filtered to a safe subset; null when none. | | `data.goal` | string \| null | yes | One sentence on what it achieves; null when none. | | `data.business_value` | string \| null | yes | Why the client wants it, as HTML; null when none. | | `data.hypothesis` | string \| null | yes | The bet it makes; null when none. | | `data.success_metrics` | string \| null | yes | How the team will know it worked, as HTML; null when none. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `in_progress`, `done` or `cancelled`. | | `data.starts_on` | string \| null | yes | When work on it starts, `Y-m-d`; null when not agreed. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.color` | string \| null | yes | The badge colour the board and the roadmap paint it in; null for the default. | | `data.position` | integer | yes | Its place among the project's epics, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the epic was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the epic last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `epic:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed or the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a glossary term Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/add-a-glossary-term `POST /v1/projects/{project}/glossary` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/glossary \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "term": "Branch", "definition": "

One physical shop of the client, with its own stock, staff and opening hours.

", "aliases": ["Site", "Location"], "source": "Client operations manual, 2025 edition" }' ``` Only `term` is required; it must be unique within the project, case-insensitively. The term starts as `draft` unless `status` says otherwise. `aliases` is kept as a list of up to ten short tags, duplicates folded regardless of case, and `related_term_ids` may name up to twenty terms of the same project, never the term itself. Answers `201` with the term and emits `glossary_term.created`. - Requires ability: `glossary:create` - Fires events: `glossary_term.created` - MCP tools: `create_glossary_term` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A glossary term to add or change. Only `term` is required on a create; the status defaults to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `term` | string | no | The word, 2 to 120 characters, unique within the project regardless of case. | | `definition` | string \| null | no | What it means, as HTML; filtered to a safe subset when read. | | `aliases` | array \| null | no | The other names it goes by, as up to ten tags of at most 80 characters each. | | `related_term_ids` | array \| null | no | The ids of up to twenty terms it leans on, each a term of the same project and never the term itself. | | `source` | string \| null | no | Where the definition comes from, up to 160 characters. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where the term stands in review, on a create only: `draft`, `in_review`, `approved` or `deprecated`. Defaults to `draft`. | ### Responses - **201**: The term, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The term's id, the value `related_term_ids` carries on other terms. | | `data.project_id` | string | yes | The project the term belongs to. | | `data.term` | string | yes | The word, unique within the project regardless of case. | | `data.definition` | string \| null | yes | What it means, as HTML filtered to a safe subset; null when none. | | `data.aliases` | array of string | yes | The other names it goes by, as short tags. | | `data.related_term_ids` | array of string | yes | The ids of the terms it leans on, each a term of the same project. | | `data.source` | string \| null | yes | Where the definition comes from: a document, a person, a standard; null when none. | | `data.status` | string | yes | Where the term stands in review: `draft`, `in_review`, `approved` or `deprecated`. | | `data.position` | integer | yes | Its place among the project's terms, from zero. | | `data.created_at` | string \| null | yes | When the term was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the term last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `glossary:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when another term of the project already uses the word, or when a related id is not a term of the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a glossary term Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/delete-a-glossary-term `DELETE /v1/glossary/{term}` ```bash curl -X DELETE https://api.example.test/v1/glossary/$TERM_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a term with its activity history; the terms that leaned on it drop the relation. Returns `204 No Content` and emits `glossary_term.deleted`. Requirements and business rules that will name terms are not here yet; once they are, a term they name will be deprecated rather than deleted. - Requires ability: `glossary:delete` - Fires events: `glossary_term.deleted` - MCP tools: `delete_glossary_term` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `term` | path | string (uuid) | yes | The term, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `glossary:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a glossary term Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/get-a-glossary-term `GET /v1/glossary/{term}` ```bash curl https://api.example.test/v1/glossary/$TERM_ID \ -H "Authorization: Bearer $TOKEN" ``` One term. A term of a project the token may not see, or a term that is not approved read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `status` is `draft`, `in_review`, `approved` or `deprecated`; `aliases` is a list of short tags and `related_term_ids` a list of ids of terms of the same project. - `definition` is HTML filtered to a safe subset. - Requires ability: `glossary:view` - MCP tools: `get_glossary_term` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `term` | path | string (uuid) | yes | The term, bound by the middleware. | ### Responses - **200**: The term. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The term's id, the value `related_term_ids` carries on other terms. | | `data.project_id` | string | yes | The project the term belongs to. | | `data.term` | string | yes | The word, unique within the project regardless of case. | | `data.definition` | string \| null | yes | What it means, as HTML filtered to a safe subset; null when none. | | `data.aliases` | array of string | yes | The other names it goes by, as short tags. | | `data.related_term_ids` | array of string | yes | The ids of the terms it leans on, each a term of the same project. | | `data.source` | string \| null | yes | Where the definition comes from: a document, a person, a standard; null when none. | | `data.status` | string | yes | Where the term stands in review: `draft`, `in_review`, `approved` or `deprecated`. | | `data.position` | integer | yes | Its place among the project's terms, from zero. | | `data.created_at` | string \| null | yes | When the term was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the term last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Glossary API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary A **glossary term** is a word the project agrees on. The thing the client calls a "branch", the analyst a "site" and the developers a "location" gets one entry with one definition, the other names it goes by as aliases, the terms it leans on, where the definition comes from and where it stands in review. Every story, requirement and test is read against these words, and Heimdall drafts in them, so the glossary is the project's own language written down once. The reads here see the terms of the projects the token may see; a client guest's token sees the approved terms only, because a draft is still the analyst's working note. A term outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Glossary page runs, under the project permissions, and announces itself as a `glossary_term.*` event. ## Endpoints - `GET /v1/glossary/{term}` — [Get a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/get-a-glossary-term) - `PATCH /v1/glossary/{term}` — [Update a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/update-a-glossary-term) - `DELETE /v1/glossary/{term}` — [Delete a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/delete-a-glossary-term) - `GET /v1/glossary/{term}/revisions` — [List a term's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-a-terms-versions) - `GET /v1/glossary/{term}/revisions/{revision}` — [Read one version of a term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/read-one-version-of-a-term) - `GET /v1/projects/{project}/glossary` — [List glossary terms](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-glossary-terms) - `POST /v1/projects/{project}/glossary` — [Add a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/add-a-glossary-term) - `POST /v1/glossary/{term}/status` — [Move a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/move-a-glossary-term) - `POST /v1/glossary/{term}/revisions/{revision}/restore` — [Restore a version of a term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/restore-a-version-of-a-term) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project a term belongs to. - [Data Entities API](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities): The other half of the dictionary, the things the project keeps records of. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on a term and who did it. - [glossary_term.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term): The lifecycle of a project's glossary: a term added, changed, moved along its review and deleted. --- # List a term's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-a-terms-versions `GET /v1/glossary/{term}/revisions` Every version the term was saved as, newest first, each with who wrote it and why, the columns it changed and the diff from the version before. - Requires ability: `glossary:view` - MCP tools: `get_glossary_term` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `term` | path | string (uuid) | yes | The term, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List glossary terms Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-glossary-terms `GET /v1/projects/{project}/glossary` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/glossary?status=approved" \ -H "Authorization: Bearer $TOKEN" ``` The terms of one project in the project's order, 25 per page (`per_page` 1 to 100). `sort_by` takes `term`, `status`, `position`, `created_at` or `updated_at`. `status` narrows the page to one status and `search` to the terms whose word, aliases or definition contain the words. A client guest's token lists the approved terms whatever `status` asks for. - Requires ability: `glossary:view-any` - MCP tools: `list_glossary_terms` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `status` | query | `draft`, `in_review`, `approved`, `deprecated` \| null | no | Narrow to one status: `draft`, `in_review`, `approved` or `deprecated`. An unknown status is refused. | | `search` | query | string \| null | no | Words the term, an alias or the definition must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of terms. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The term's id, the value `related_term_ids` carries on other terms. | | `data[].project_id` | string | yes | The project the term belongs to. | | `data[].term` | string | yes | The word, unique within the project regardless of case. | | `data[].definition` | string \| null | yes | What it means, as HTML filtered to a safe subset; null when none. | | `data[].aliases` | array of string | yes | The other names it goes by, as short tags. | | `data[].related_term_ids` | array of string | yes | The ids of the terms it leans on, each a term of the same project. | | `data[].source` | string \| null | yes | Where the definition comes from: a document, a person, a standard; null when none. | | `data[].status` | string | yes | Where the term stands in review: `draft`, `in_review`, `approved` or `deprecated`. | | `data[].position` | integer | yes | Its place among the project's terms, from zero. | | `data[].created_at` | string \| null | yes | When the term was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the term last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is not one of the four statuses or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a glossary term Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/move-a-glossary-term `POST /v1/glossary/{term}/status` ```bash curl -X POST https://api.example.test/v1/glossary/$TERM_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "in_review"}' ``` Moves the term along its review: `draft`, `in_review`, `approved` and `deprecated` may each move to any other, except that a deprecated term can only go back to `draft`. A client guest's token stops seeing a term the moment it leaves `approved`. Answers `200` with the term and emits `glossary_term.status_changed` with the status before and after; a move to the status the term is in already answers `200` and emits nothing. - Requires ability: `glossary:update` - Fires events: `glossary_term.status_changed` - MCP tools: `update_glossary_term` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `term` | path | string (uuid) | yes | The term, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the artefact to. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the artefact goes: `draft`, `in_review`, `approved` or `deprecated`. A deprecated artefact can only move back to `draft`. | ### Responses - **200**: The term, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The term's id, the value `related_term_ids` carries on other terms. | | `data.project_id` | string | yes | The project the term belongs to. | | `data.term` | string | yes | The word, unique within the project regardless of case. | | `data.definition` | string \| null | yes | What it means, as HTML filtered to a safe subset; null when none. | | `data.aliases` | array of string | yes | The other names it goes by, as short tags. | | `data.related_term_ids` | array of string | yes | The ids of the terms it leans on, each a term of the same project. | | `data.source` | string \| null | yes | Where the definition comes from: a document, a person, a standard; null when none. | | `data.status` | string | yes | Where the term stands in review: `draft`, `in_review`, `approved` or `deprecated`. | | `data.position` | integer | yes | Its place among the project's terms, from zero. | | `data.created_at` | string \| null | yes | When the term was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the term last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `glossary:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, or when the lifecycle refuses the move. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Read one version of a term Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/read-one-version-of-a-term `GET /v1/glossary/{term}/revisions/{revision}` One version with its full snapshot, the diff from the version before, who wrote it and, for a restore, the version it put back. - Requires ability: `glossary:view` - MCP tools: `get_glossary_term` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `term` | path | string (uuid) | yes | The term, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version with its snapshot and diff. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a term Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/restore-a-version-of-a-term `POST /v1/glossary/{term}/revisions/{revision}/restore` Fills the version's snapshot back onto the term and writes a new version that points at it; nothing earlier is rewritten. - Requires ability: `glossary:update` - Fires events: `glossary_term.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `term` | path | string (uuid) | yes | The term, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version written, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a glossary term Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/update-a-glossary-term `PATCH /v1/glossary/{term}` ```bash curl -X PATCH https://api.example.test/v1/glossary/$TERM_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"aliases": ["Site", "Location", "Store"], "source": null}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. A new word must be free within the project. The status does not change here; the move endpoint owns it. Answers `200` with the term and emits `glossary_term.updated` with the columns that changed, each with its old and new value, the aliases and the related ids as lists; an update that changes nothing answers `200` and emits nothing. - Requires ability: `glossary:update` - Fires events: `glossary_term.updated` - MCP tools: `update_glossary_term` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `term` | path | string (uuid) | yes | The term, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A glossary term to add or change. Only `term` is required on a create; the status defaults to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `term` | string | no | The word, 2 to 120 characters, unique within the project regardless of case. | | `definition` | string \| null | no | What it means, as HTML; filtered to a safe subset when read. | | `aliases` | array \| null | no | The other names it goes by, as up to ten tags of at most 80 characters each. | | `related_term_ids` | array \| null | no | The ids of up to twenty terms it leans on, each a term of the same project and never the term itself. | | `source` | string \| null | no | Where the definition comes from, up to 160 characters. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where the term stands in review, on a create only: `draft`, `in_review`, `approved` or `deprecated`. Defaults to `draft`. | ### Responses - **200**: The term. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The term's id, the value `related_term_ids` carries on other terms. | | `data.project_id` | string | yes | The project the term belongs to. | | `data.term` | string | yes | The word, unique within the project regardless of case. | | `data.definition` | string \| null | yes | What it means, as HTML filtered to a safe subset; null when none. | | `data.aliases` | array of string | yes | The other names it goes by, as short tags. | | `data.related_term_ids` | array of string | yes | The ids of the terms it leans on, each a term of the same project. | | `data.source` | string \| null | yes | Where the definition comes from: a document, a person, a standard; null when none. | | `data.status` | string | yes | Where the term stands in review: `draft`, `in_review`, `approved` or `deprecated`. | | `data.position` | integer | yes | Its place among the project's terms, from zero. | | `data.created_at` | string \| null | yes | When the term was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the term last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `glossary:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when the new word is already used by another term of the project, or when a related id is not a term of the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Create a group Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/create-a-group `POST /v1/groups` ```bash curl -X POST https://api.example.test/v1/groups \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "code": "incident-commanders", "name": "Incident Commanders", "permissions": ["incident:command", "incident:publish-update"] }' ``` > **Choose the code deliberately.** `code` is unique within the team and **cannot be changed afterwards**: permissions are addressed by it. Only `name` and the permission set are mutable. A group starts empty. Add people with the members endpoint; membership is deliberately not a field here, because replacing who is in a group and naming it are different operations with different consequences. Answers `201` with the group and its permissions. - Requires ability: `group:create` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A group's team, code, name and permissions. On create `team_id`, `code` and `name` are required; on update `team_id` and `code` are prohibited (a group is neither moved nor renamed by code) and `name` and `permissions` are optional. Membership is set with the members endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `team_id` | string (uuid) | yes | The team to create the group in; the caller must belong to it, and a team outside their account answers 404. Prohibited on update: a group cannot be moved between teams. | | `code` | string | yes | The machine name, up to 255 letters, numbers, dashes and underscores, unique within the team and immutable afterwards because permissions are addressed by it. Prohibited on update. | | `name` | string | yes | The display name, up to 255 characters. | | `permissions` | array of `client:view-any`, `client:view`, `client:create`, `client:update`, `client:delete`, `project:view-any`, `project:view`, `project:create`, `project:update`, `project:delete`, `project-member:view-any`, `project-member:view`, `project-member:create`, `project-member:update`, `project-member:delete`, `component:view-any`, `component:view`, `component:create`, `component:update`, `component:delete`, `environment:view-any`, `environment:view`, `environment:create`, `environment:update`, `environment:delete`, `environment:deploy`, `environment:freeze`, `repository:view-any`, `repository:view`, `repository:create`, `repository:update`, `repository:delete`, `stakeholder:view-any`, `stakeholder:view`, `stakeholder:create`, `stakeholder:update`, `stakeholder:delete`, `persona:view-any`, `persona:view`, `persona:create`, `persona:update`, `persona:delete`, `glossary:view-any`, `glossary:view`, `glossary:create`, `glossary:update`, `glossary:delete`, `data-entity:view-any`, `data-entity:view`, `data-entity:create`, `data-entity:update`, `data-entity:delete`, `design-reference:view-any`, `design-reference:view`, `design-reference:create`, `design-reference:update`, `design-reference:delete`, `epic:view-any`, `epic:view`, `epic:create`, `epic:update`, `epic:delete`, `epic:submit`, `epic:approve`, `work-item:view-any`, `work-item:view`, `work-item:create`, `work-item:update`, `work-item:delete`, `work-item:transition`, `work-item:assign`, `work-item:rank`, `requirement:view-any`, `requirement:view`, `requirement:create`, `requirement:update`, `requirement:delete`, `requirement:submit`, `requirement:approve`, `business-rule:view-any`, `business-rule:view`, `business-rule:create`, `business-rule:update`, `business-rule:delete`, `business-rule:submit`, `business-rule:approve`, `use-case:view-any`, `use-case:view`, `use-case:create`, `use-case:update`, `use-case:delete`, `raid:view-any`, `raid:view`, `raid:create`, `raid:update`, `raid:delete`, `decision:view-any`, `decision:view`, `decision:create`, `decision:update`, `decision:delete`, `link:view-any`, `link:view`, `link:create`, `link:update`, `link:delete`, `baseline:view-any`, `baseline:view`, `baseline:create`, `baseline:update`, `baseline:delete`, `baseline:freeze`, `approval:view-any`, `approval:view`, `approval:decide`, `approval:withdraw`, `approval:manage-policies`, `change-request:view-any`, `change-request:view`, `change-request:create`, `change-request:update`, `change-request:delete`, `change-request:analyse`, `change-request:approve`, `change-request:verify`, `sprint:view-any`, `sprint:view`, `sprint:create`, `sprint:update`, `sprint:delete`, `sprint:start`, `sprint:close`, `time-log:view-any`, `time-log:view`, `time-log:create`, `time-log:update`, `time-log:delete`, `time-log:approve`, `test-suite:view-any`, `test-suite:view`, `test-suite:create`, `test-suite:update`, `test-suite:delete`, `test-case:view-any`, `test-case:view`, `test-case:create`, `test-case:update`, `test-case:delete`, `test-case:approve`, `test-plan:view-any`, `test-plan:view`, `test-plan:create`, `test-plan:update`, `test-plan:delete`, `test-plan:approve`, `test-run:view-any`, `test-run:view`, `test-run:create`, `test-run:update`, `test-run:delete`, `test-run:execute`, `test-run:sign-off`, `release:view-any`, `release:view`, `release:create`, `release:update`, `release:delete`, `release:freeze`, `release:approve`, `release:publish`, `release:rollback`, `deployment:view-any`, `deployment:view`, `deployment:create`, `deployment:update`, `deployment:delete`, `deployment:record`, `deployment:promote`, `incident:view-any`, `incident:view`, `incident:create`, `incident:update`, `incident:delete`, `incident:command`, `incident:resolve`, `incident:publish-update`, `ticket:view-any`, `ticket:view`, `ticket:create`, `ticket:update`, `ticket:delete`, `ticket:respond`, `ticket:convert`, `maintenance-window:view-any`, `maintenance-window:view`, `maintenance-window:create`, `maintenance-window:update`, `maintenance-window:delete`, `knowledge:view-any`, `knowledge:view`, `knowledge:create`, `knowledge:update`, `knowledge:delete`, `document:view-any`, `document:view`, `document:create`, `document:update`, `document:delete`, `document:render`, `document:finalise`, `import:view-any`, `import:view`, `import:create`, `import:update`, `import:delete`, `comment:view-any`, `comment:view`, `comment:create`, `comment:update`, `comment:delete`, `comment:internal`, `attachment:view-any`, `attachment:view`, `attachment:create`, `attachment:update`, `attachment:delete`, `report:read`, `dashboard:read`, `activity:read`, `ai:use`, `ai:manage-budgets`, `team:view-any`, `team:view`, `team:create`, `team:update`, `team:delete`, `team-member:view-any`, `team-member:view`, `team-member:invite`, `team-member:remove`, `team-member:update-role`, `role:view-any`, `role:view`, `role:create`, `role:update`, `role:delete`, `group:view-any`, `group:view`, `group:create`, `group:update`, `group:delete`, `token:view-any`, `token:view`, `token:create`, `token:update`, `token:delete`, `webhook-endpoint:view-any`, `webhook-endpoint:view`, `webhook-endpoint:create`, `webhook-endpoint:update`, `webhook-endpoint:delete`, `webhook-delivery:view-any`, `webhook-delivery:view`, `webhook-delivery:retry`, `integration:manage` | no | The permission codes the group grants. Sending it sets the group's permissions to exactly this list; omit the key to leave the existing set untouched. Defaults to none on create. | ### Responses - **201**: The group as a 201. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: when `team_id` is not one of the caller's teams. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a group with that code already exists in the team (`error.context.code` names it), or a permission code is unknown. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a group Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/delete-a-group `DELETE /v1/groups/{group}` ```bash curl -X DELETE https://api.example.test/v1/groups/$GROUP_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Returns `204 No Content`. Everyone in the group loses whatever it granted them, but stays in the team. - Requires ability: `group:delete` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `group` | path | string (uuid) | yes | The group, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a group Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/get-a-group `GET /v1/groups/{group}` One group with its `permissions` and `member_ids`. - Requires ability: `group:view` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `group` | path | string (uuid) | yes | The group, resolved by the route binder. | ### Responses - **200**: The group with its permissions and members. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Groups API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/groups A **group** is a named bundle of permissions on a team that several members hold at once: put the three people who run the on-call rota into an `incident-commanders` group and they share its permissions, take one out and they keep only what their own role gives them. Groups sit beside [roles](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles), which are assigned one per member; a group is the way to grant the same extra permissions to a set of people without editing each role. Each group carries `permissions`, the exact ability strings a token can hold, and `member_ids`, the members in it. Both are returned when a group is read individually or after a write and omitted from the list so a listing stays one query. Membership is replaced as a whole with `PUT …/members`, so a sync from your own directory sends the full list each time rather than adding and removing one by one. Deleting a group removes what it granted from everyone in it; nobody leaves the team. Visibility is the union of every team the caller owns or belongs to, and a group on a team outside that set answers `404 RESOURCE_NOT_FOUND` rather than `403`, so the API never confirms that a team exists. Group changes raise no webhook events of their own. ## Endpoints - `GET /v1/groups` — [List groups](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/list-groups) - `POST /v1/groups` — [Create a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/create-a-group) - `GET /v1/groups/{group}` — [Get a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/get-a-group) - `PATCH /v1/groups/{group}` — [Update a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/update-a-group) - `DELETE /v1/groups/{group}` — [Delete a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/delete-a-group) - `PUT /v1/groups/{group}/members` — [Replace a group's members](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/replace-a-groups-members) ## Related - [Teams API](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams): A team is the tenant: the container every project, role, group, token and webhook endpoint hangs off. - [Roles API](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles): Per-member permission sets - [Team Members API](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members): Team members are the people inside a team: the owner and everyone invited to work in it, staff and client guests alike. --- # List groups Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/list-groups `GET /v1/groups` ```bash curl https://api.example.test/v1/groups \ -H "Authorization: Bearer $TOKEN" ``` Every group across the teams the caller sees. Rows carry neither `permissions` nor `member_ids`; read a group individually for those. Not paginated. - Requires ability: `group:view-any` - MCP tools: `list_groups` ### Responses - **200**: Array of `GroupResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The group's id. | | `data[].team_id` | string | yes | The team the group belongs to. | | `data[].code` | string | yes | The machine name, unique within the team and immutable after creation, because permissions are addressed by it. | | `data[].name` | string | yes | The display name. | | `data[].permissions` | array of string | no | The permission codes the group grants. Returned when the group is read individually or after a write, omitted from list responses so a listing stays one query. | | `data[].member_ids` | array of string | no | The users in the group. Returned after a member sync and on a detail read, omitted elsewhere. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Replace a group's members Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/replace-a-groups-members `PUT /v1/groups/{group}/members` ```bash curl -X PUT https://api.example.test/v1/groups/$GROUP_ID/members \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"user_ids": ["2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "4b8e2f6a-1c3d-4e5f-9a7b-8c9d0e1f2a3b"]}' ``` This is a **sync**, not an add. The array replaces the membership entirely: anyone omitted is removed, and `[]` empties the group. That is deliberate and differs from the permission field above: clearing a group is a legitimate thing to ask for, whereas silently stripping every permission because a field was blank is not. Every id must already belong to the group's team. A group grants permissions inside one tenant, so an outsider is refused naming the offending ids; the whole call fails rather than partly applying, so you never end up with a membership you did not ask for. Answers `200` with the group, its `permissions` and its `member_ids`. - Requires ability: `group:update` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `group` | path | string (uuid) | yes | The group, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The group's complete membership after the call. A sync, not an add: anyone omitted is removed, and `[]` empties the group. | Field | Type | Required | Description | | --- | --- | --- | --- | | `user_ids` | array of string (uuid) | yes | Every user the group should have afterwards. Must be present; `[]` empties the group. Each id must already belong to the group's team, or the whole call is refused naming the outsiders. | ### Responses - **200**: The group with its permissions and members as they now stand. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when any id is not a member of the group's team; `error.message` lists the outsiders. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a group Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/update-a-group `PATCH /v1/groups/{group}` ```bash curl -X PATCH https://api.example.test/v1/groups/$GROUP_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"name": "On-call Commanders"}' ``` > **Permissions replace, they do not merge.** Sending `permissions` sets the group's permissions to exactly that list. **Omit the key entirely** to leave the existing set untouched while changing only the name. `code` and `team_id` are prohibited: a group is neither renamed by code nor moved between teams. Answers `200` with the group and its permissions. - Requires ability: `group:update` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `group` | path | string (uuid) | yes | The group, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A group's team, code, name and permissions. On create `team_id`, `code` and `name` are required; on update `team_id` and `code` are prohibited (a group is neither moved nor renamed by code) and `name` and `permissions` are optional. Membership is set with the members endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `team_id` | string (uuid) | yes | The team to create the group in; the caller must belong to it, and a team outside their account answers 404. Prohibited on update: a group cannot be moved between teams. | | `code` | string | yes | The machine name, up to 255 letters, numbers, dashes and underscores, unique within the team and immutable afterwards because permissions are addressed by it. Prohibited on update. | | `name` | string | yes | The display name, up to 255 characters. | | `permissions` | array of `client:view-any`, `client:view`, `client:create`, `client:update`, `client:delete`, `project:view-any`, `project:view`, `project:create`, `project:update`, `project:delete`, `project-member:view-any`, `project-member:view`, `project-member:create`, `project-member:update`, `project-member:delete`, `component:view-any`, `component:view`, `component:create`, `component:update`, `component:delete`, `environment:view-any`, `environment:view`, `environment:create`, `environment:update`, `environment:delete`, `environment:deploy`, `environment:freeze`, `repository:view-any`, `repository:view`, `repository:create`, `repository:update`, `repository:delete`, `stakeholder:view-any`, `stakeholder:view`, `stakeholder:create`, `stakeholder:update`, `stakeholder:delete`, `persona:view-any`, `persona:view`, `persona:create`, `persona:update`, `persona:delete`, `glossary:view-any`, `glossary:view`, `glossary:create`, `glossary:update`, `glossary:delete`, `data-entity:view-any`, `data-entity:view`, `data-entity:create`, `data-entity:update`, `data-entity:delete`, `design-reference:view-any`, `design-reference:view`, `design-reference:create`, `design-reference:update`, `design-reference:delete`, `epic:view-any`, `epic:view`, `epic:create`, `epic:update`, `epic:delete`, `epic:submit`, `epic:approve`, `work-item:view-any`, `work-item:view`, `work-item:create`, `work-item:update`, `work-item:delete`, `work-item:transition`, `work-item:assign`, `work-item:rank`, `requirement:view-any`, `requirement:view`, `requirement:create`, `requirement:update`, `requirement:delete`, `requirement:submit`, `requirement:approve`, `business-rule:view-any`, `business-rule:view`, `business-rule:create`, `business-rule:update`, `business-rule:delete`, `business-rule:submit`, `business-rule:approve`, `use-case:view-any`, `use-case:view`, `use-case:create`, `use-case:update`, `use-case:delete`, `raid:view-any`, `raid:view`, `raid:create`, `raid:update`, `raid:delete`, `decision:view-any`, `decision:view`, `decision:create`, `decision:update`, `decision:delete`, `link:view-any`, `link:view`, `link:create`, `link:update`, `link:delete`, `baseline:view-any`, `baseline:view`, `baseline:create`, `baseline:update`, `baseline:delete`, `baseline:freeze`, `approval:view-any`, `approval:view`, `approval:decide`, `approval:withdraw`, `approval:manage-policies`, `change-request:view-any`, `change-request:view`, `change-request:create`, `change-request:update`, `change-request:delete`, `change-request:analyse`, `change-request:approve`, `change-request:verify`, `sprint:view-any`, `sprint:view`, `sprint:create`, `sprint:update`, `sprint:delete`, `sprint:start`, `sprint:close`, `time-log:view-any`, `time-log:view`, `time-log:create`, `time-log:update`, `time-log:delete`, `time-log:approve`, `test-suite:view-any`, `test-suite:view`, `test-suite:create`, `test-suite:update`, `test-suite:delete`, `test-case:view-any`, `test-case:view`, `test-case:create`, `test-case:update`, `test-case:delete`, `test-case:approve`, `test-plan:view-any`, `test-plan:view`, `test-plan:create`, `test-plan:update`, `test-plan:delete`, `test-plan:approve`, `test-run:view-any`, `test-run:view`, `test-run:create`, `test-run:update`, `test-run:delete`, `test-run:execute`, `test-run:sign-off`, `release:view-any`, `release:view`, `release:create`, `release:update`, `release:delete`, `release:freeze`, `release:approve`, `release:publish`, `release:rollback`, `deployment:view-any`, `deployment:view`, `deployment:create`, `deployment:update`, `deployment:delete`, `deployment:record`, `deployment:promote`, `incident:view-any`, `incident:view`, `incident:create`, `incident:update`, `incident:delete`, `incident:command`, `incident:resolve`, `incident:publish-update`, `ticket:view-any`, `ticket:view`, `ticket:create`, `ticket:update`, `ticket:delete`, `ticket:respond`, `ticket:convert`, `maintenance-window:view-any`, `maintenance-window:view`, `maintenance-window:create`, `maintenance-window:update`, `maintenance-window:delete`, `knowledge:view-any`, `knowledge:view`, `knowledge:create`, `knowledge:update`, `knowledge:delete`, `document:view-any`, `document:view`, `document:create`, `document:update`, `document:delete`, `document:render`, `document:finalise`, `import:view-any`, `import:view`, `import:create`, `import:update`, `import:delete`, `comment:view-any`, `comment:view`, `comment:create`, `comment:update`, `comment:delete`, `comment:internal`, `attachment:view-any`, `attachment:view`, `attachment:create`, `attachment:update`, `attachment:delete`, `report:read`, `dashboard:read`, `activity:read`, `ai:use`, `ai:manage-budgets`, `team:view-any`, `team:view`, `team:create`, `team:update`, `team:delete`, `team-member:view-any`, `team-member:view`, `team-member:invite`, `team-member:remove`, `team-member:update-role`, `role:view-any`, `role:view`, `role:create`, `role:update`, `role:delete`, `group:view-any`, `group:view`, `group:create`, `group:update`, `group:delete`, `token:view-any`, `token:view`, `token:create`, `token:update`, `token:delete`, `webhook-endpoint:view-any`, `webhook-endpoint:view`, `webhook-endpoint:create`, `webhook-endpoint:update`, `webhook-endpoint:delete`, `webhook-delivery:view-any`, `webhook-delivery:view`, `webhook-delivery:retry`, `integration:manage` | no | The permission codes the group grants. Sending it sets the group's permissions to exactly this list; omit the key to leave the existing set untouched. Defaults to none on create. | ### Responses - **200**: The updated group. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | any | yes | The response's payload. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `code` or `team_id` is sent, or a permission code is unknown. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Accept a suggestion Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/accept-a-suggestion `POST /v1/suggestions/{suggestion}/accept` ```bash curl -X POST https://api.example.test/v1/suggestions/$SUGGESTION_ID/accept \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "payload": { "title": "Spend points at checkout", "as_a": "returning shopper", "i_want": "to pay part of my order with points", "so_that": "my loyalty is worth something at the till", "description": "

Points are applied after every discount and before payment.

", "criteria": [ "Given a shopper with 500 points, when they apply 200 points, then the total falls by their value" ] } }' ``` Writes the record the suggestion proposes through the record's own create action, as proposed when the body is empty or with the fields in `payload` when it is sent; unknown fields are dropped and a list field is an array of strings. The record's first version is written with the reason `import`, by Heimdall and accepted by the token's holder, and the record's own `created` event goes out beside `import.suggestion_accepted`. A story accepted after its epic hangs under the epic the parent suggestion wrote. An improvement is kept as a note without a record. The suggestion is marked `accepted` or `edited`, and a batch with no pending suggestion left is marked `accepted`. - Requires ability: `import:update` - Fires events: `import.suggestion_accepted` - MCP tools: `decide_suggestion` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `suggestion` | path | string (uuid) | yes | The suggestion, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) Accept a suggestion: send nothing to write the record as Heimdall proposed it, or `payload` with the fields changed to write it with those edits. Unknown fields are dropped; the fields of each kind are the ones the suggestion carries. | Field | Type | Required | Description | | --- | --- | --- | --- | | `payload` | array of string | no | The fields of the record, keyed as the suggestion's payload is; omit to accept as proposed. A list field (criteria, examples, steps, aliases) is sent as an array of strings. | ### Responses - **200**: The suggestion, decided, with the record it wrote. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The suggestion's id. | | `data.project_id` | string | yes | The project the brief was imported into. | | `data.batch_id` | string \| null | yes | The import batch it came from; null for a note from a research sweep. | | `data.kind` | string | yes | What it proposes: `epic`, `story`, `functional_requirement`, `non_functional_requirement`, `business_rule`, `use_case`, `glossary_term`, `persona`, `stakeholder`, `risk`, `assumption`, `decision`, `improvement` or `research`. | | `data.position` | integer | yes | Its place in the batch, from one, in the order Heimdall proposed. | | `data.parent_id` | string \| null | yes | The suggestion it hangs under, such as a story's epic; null at the top. | | `data.title` | string | yes | Its name, from the payload field its kind names: `title`, `name` or `term`. | | `data.payload` | array \| null | yes | The fields of the record it would become, keyed as the record's own editor names them; the edited fields once accepted with edits. | | `data.confidence` | number \| null | yes | How directly the brief supports it, 0 to 1; null when the model gave none. | | `data.rationale` | string \| null | yes | One sentence on why Heimdall proposed it. | | `data.source_quote` | string \| null | yes | The passage of the brief it drew on, word for word; null for an improvement. | | `data.source_span` | array \| null | yes | Where that passage sits in the brief, `{ "start": …, "end": … }` as character offsets; null when the quote was not found. | | `data.sources` | array \| null | yes | The sources a research note cites; null otherwise. | | `data.voice` | string \| null | yes | The voice of the council that proposed it (`odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`); null for a brief import's or a research run's suggestion. | | `data.status` | string | yes | What was decided: `pending`, `accepted`, `edited`, `merged`, `rejected` or `dismissed`. | | `data.duplicate_type` | string \| null | yes | The alias of the record it may duplicate, or was merged into: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `glossary-term`, `persona`, `stakeholder`, `raid-item` or `decision`; null when none. | | `data.duplicate_id` | string \| null | yes | That record's id; null when none. | | `data.created_type` | string \| null | yes | The alias of the record the decision wrote; null until accepted, and for a note. | | `data.created_id` | string \| null | yes | That record's id; null until accepted. | | `data.decided_by` | string \| null | yes | The person who decided; null while pending or when the account is gone. | | `data.decider_name` | string \| null | yes | That person's name, when the decider was loaded; null otherwise. | | `data.decided_at` | string \| null | yes | When it was decided, ISO 8601; null while pending. | | `data.created_at` | string \| null | yes | When Heimdall proposed it, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:update` and the record's own create permission. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the suggestion is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the suggestion was already decided, its batch is not ready, `payload` is not an object, or the fields fail the record's own rules (a missing title, for one). - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Discard an import Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/discard-an-import `POST /v1/imports/{batch}/discard` ```bash curl -X POST https://api.example.test/v1/imports/$BATCH_ID/discard \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Drops a batch: its suggestions stay readable but can no longer be decided, a reading still on the queue writes nothing when it lands, and `import.discarded` goes out. The records accepted before the batch was discarded stay where they are. - Requires ability: `import:delete` - Fires events: `import.discarded` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `batch` | path | string (uuid) | yes | The batch, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The batch, discarded. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The batch's id. | | `data.project_id` | string | yes | The project the brief was imported into. | | `data.source` | string | yes | Where the words came from: `pasted_text`, `upload`, `url` or `meeting_notes`. | | `data.status` | string | yes | Where the reading stands: `queued`, `running`, `ready`, `accepted` (every suggestion decided), `discarded` or `failed`. | | `data.title` | string | yes | What the batch is called. | | `data.excerpt` | string | yes | The opening words of the brief, for a list. | | `data.text` | string | no | The brief, as it was given; present on a single batch. | | `data.suggestion_count` | integer | yes | How many suggestions Heimdall proposed; 0 until the reading is done. | | `data.pending_count` | integer | no | How many suggestions still wait for a decision; present on a single batch. | | `data.provider` | string \| null | yes | The provider that answered, by its configuration key; null until the reading is done. | | `data.model` | string \| null | yes | The model that answered; null until the reading is done. | | `data.usage_id` | string \| null | yes | The ledger row that filed the call's cost; null until the reading is done or when the row is gone. | | `data.failure_reason` | string \| null | yes | Why the reading failed, as a person reads it; null otherwise. | | `data.created_by` | string \| null | yes | The person who started it; null when the account is gone. | | `data.creator_name` | string \| null | yes | That person's name, when the creator was loaded; null otherwise. | | `data.started_at` | string \| null | yes | When Heimdall began reading, ISO 8601; null while queued. | | `data.finished_at` | string \| null | yes | When the reading ended, well or badly, or the batch was discarded, ISO 8601; null meanwhile. | | `data.created_at` | string \| null | yes | When the batch was started, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | | `data.suggestions` | array of array of any | no | The suggestions, in order; present on a single batch. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the batch is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the batch was already discarded. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get an import Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/get-an-import `GET /v1/imports/{batch}` ```bash curl https://api.example.test/v1/imports/$BATCH_ID \ -H "Authorization: Bearer $TOKEN" ``` One batch with the brief's words, how many suggestions still wait, and every suggestion Heimdall proposed in order, each with its kind, its fields, the suggestion it hangs under, how sure the model was and why, the passage of the brief it drew on, the record it may duplicate and what was decided. - Requires ability: `import:view` - MCP tools: `get_council_session`, `get_import_batch` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `batch` | path | string (uuid) | yes | The batch, bound by the middleware. | ### Responses - **200**: The batch with its suggestions. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The batch's id. | | `data.project_id` | string | yes | The project the brief was imported into. | | `data.source` | string | yes | Where the words came from: `pasted_text`, `upload`, `url` or `meeting_notes`. | | `data.status` | string | yes | Where the reading stands: `queued`, `running`, `ready`, `accepted` (every suggestion decided), `discarded` or `failed`. | | `data.title` | string | yes | What the batch is called. | | `data.excerpt` | string | yes | The opening words of the brief, for a list. | | `data.text` | string | no | The brief, as it was given; present on a single batch. | | `data.suggestion_count` | integer | yes | How many suggestions Heimdall proposed; 0 until the reading is done. | | `data.pending_count` | integer | no | How many suggestions still wait for a decision; present on a single batch. | | `data.provider` | string \| null | yes | The provider that answered, by its configuration key; null until the reading is done. | | `data.model` | string \| null | yes | The model that answered; null until the reading is done. | | `data.usage_id` | string \| null | yes | The ledger row that filed the call's cost; null until the reading is done or when the row is gone. | | `data.failure_reason` | string \| null | yes | Why the reading failed, as a person reads it; null otherwise. | | `data.created_by` | string \| null | yes | The person who started it; null when the account is gone. | | `data.creator_name` | string \| null | yes | That person's name, when the creator was loaded; null otherwise. | | `data.started_at` | string \| null | yes | When Heimdall began reading, ISO 8601; null while queued. | | `data.finished_at` | string \| null | yes | When the reading ended, well or badly, or the batch was discarded, ISO 8601; null meanwhile. | | `data.created_at` | string \| null | yes | When the batch was started, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | | `data.suggestions` | array of array of any | no | The suggestions, in order; present on a single batch. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the batch is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Import a brief Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/import-a-brief `POST /v1/projects/{project}/imports` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/imports \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "title": "Customer portal brief", "text": "The client runs a loyalty programme. Shoppers earn points on every order and spend them at checkout.\n\nGold members get free delivery. Points expire after twelve months.", "source": "pasted_text" }' ``` Hands a brief to Heimdall: writes the batch as queued, puts the reading on the queue as the token's holder and emits `import.started`. `title` names the batch; `text` is the brief as plain text or Markdown, at least 40 and at most 60,000 characters; `source` is `pasted_text` (the default) or `meeting_notes`, since uploads go through the application. Poll the batch until its status is `ready` (then read its suggestions) or `failed` (with the reason, and start it again). Answers `202` with the batch, queued. - Requires ability: `import:create` - Fires events: `import.started` - MCP tools: `start_brief_import` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A brief to import: what to call the batch, the words of the brief as plain text or Markdown, and where they came from. Heimdall reads it on the queue; poll the batch until it is ready. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | yes | What the batch is called on the list, up to 160 characters. | | `text` | string | yes | The brief, as plain text or Markdown: at least 40 and at most 60,000 characters. | | `source` | `pasted_text`, `meeting_notes` | no | Where the words came from: `pasted_text` (the default) or `meeting_notes`. Uploads go through the application. | ### Responses - **202**: The batch, queued, `202`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The batch's id. | | `data.project_id` | string | yes | The project the brief was imported into. | | `data.source` | string | yes | Where the words came from: `pasted_text`, `upload`, `url` or `meeting_notes`. | | `data.status` | string | yes | Where the reading stands: `queued`, `running`, `ready`, `accepted` (every suggestion decided), `discarded` or `failed`. | | `data.title` | string | yes | What the batch is called. | | `data.excerpt` | string | yes | The opening words of the brief, for a list. | | `data.text` | string | no | The brief, as it was given; present on a single batch. | | `data.suggestion_count` | integer | yes | How many suggestions Heimdall proposed; 0 until the reading is done. | | `data.pending_count` | integer | no | How many suggestions still wait for a decision; present on a single batch. | | `data.provider` | string \| null | yes | The provider that answered, by its configuration key; null until the reading is done. | | `data.model` | string \| null | yes | The model that answered; null until the reading is done. | | `data.usage_id` | string \| null | yes | The ledger row that filed the call's cost; null until the reading is done or when the row is gone. | | `data.failure_reason` | string \| null | yes | Why the reading failed, as a person reads it; null otherwise. | | `data.created_by` | string \| null | yes | The person who started it; null when the account is gone. | | `data.creator_name` | string \| null | yes | That person's name, when the creator was loaded; null otherwise. | | `data.started_at` | string \| null | yes | When Heimdall began reading, ISO 8601; null while queued. | | `data.finished_at` | string \| null | yes | When the reading ended, well or badly, or the batch was discarded, ISO 8601; null meanwhile. | | `data.created_at` | string \| null | yes | When the batch was started, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | | `data.suggestions` | array of array of any | no | The suggestions, in order; present on a single batch. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, the holder's role on the project does not carry `import:create`, or the project's assists are off. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the project is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the title is missing or over 160 characters, the text is shorter than 40 or longer than 60,000 characters, or the source is not one of the two offered. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Imports API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/imports A **brief import** hands a client's brief to Heimdall and gets back **suggestions**: the epics, stories, requirements, business rules, use cases, glossary terms, personas, stakeholders, risks, assumptions and decisions a good analyst would draft from it, each with how sure Heimdall was, why, and the passage of the brief it drew on, plus **improvements**, the gaps and questions a project of this kind leaves open. Nothing is written to the project by the reading. A person decides each suggestion: **accept** writes the record through its own create action, as proposed or with edits, so its number, validation, first version (reason `import`, written by Heimdall and accepted by the person) and `created` event are the ones every other write gets; **merge** points the suggestion at a record the project already holds; **reject** drops it. A batch whose every suggestion is decided is marked accepted; a batch a person drops is discarded. The reading runs on the queue as the person who started it and takes a minute or more: post the brief, poll the batch until its status is `ready` or `failed`, then read and decide its suggestions. Tokens read and write imports as staff with the `import:*` abilities; a client guest's token is refused everywhere here. Starting an import needs the project's AI assists on and room in the team's monthly AI budget, which the reading spends. ## Endpoints - `GET /v1/projects/{project}/imports` — [List a project's imports](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-a-projects-imports) - `POST /v1/projects/{project}/imports` — [Import a brief](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/import-a-brief) - `GET /v1/imports/{batch}` — [Get an import](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/get-an-import) - `GET /v1/imports/{batch}/suggestions` — [List an import's suggestions](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-an-imports-suggestions) - `POST /v1/imports/{batch}/discard` — [Discard an import](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/discard-an-import) - `POST /v1/suggestions/{suggestion}/accept` — [Accept a suggestion](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/accept-a-suggestion) - `POST /v1/suggestions/{suggestion}/reject` — [Reject a suggestion](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/reject-a-suggestion) - `POST /v1/suggestions/{suggestion}/merge` — [Merge a suggestion](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/merge-a-suggestion) ## Related - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics): [work items](/api/v1/reference/work-items), [requirements](/api/v1/reference/requirements), [business rules](/api/v1/reference/business-rules), [use cases](/api/v1/reference/use-cases), [glossary](/api/v1/reference/glossary), [personas](/api/v1/reference/personas), [stakeholders](/api/v1/reference/stakeholders), [the RAID log](/api/v1/reference/raid-log) and [decisions](/api/v1/reference/decisions): the records an accepted suggestion becomes. - [import.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import): The life of a brief import: a brief handed to Heimdall, read into suggestions or failed, discarded, and each suggestion accepted, rejected or merged by a person. --- # List a project's imports Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-a-projects-imports `GET /v1/projects/{project}/imports` ```bash curl https://api.example.test/v1/projects/$PROJECT_ID/imports \ -H "Authorization: Bearer $TOKEN" ``` Every brief handed to Heimdall for the project, newest first, each with where its words came from, where the reading stands, how many suggestions it produced, who started it and when. The words and the suggestions are on the single batch. - Requires ability: `import:view-any` - MCP tools: `list_council_sessions`, `list_import_batches`, `list_research_notes` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | ### Responses - **200**: Array of `ImportBatchResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The batch's id. | | `data[].project_id` | string | yes | The project the brief was imported into. | | `data[].source` | string | yes | Where the words came from: `pasted_text`, `upload`, `url` or `meeting_notes`. | | `data[].status` | string | yes | Where the reading stands: `queued`, `running`, `ready`, `accepted` (every suggestion decided), `discarded` or `failed`. | | `data[].title` | string | yes | What the batch is called. | | `data[].excerpt` | string | yes | The opening words of the brief, for a list. | | `data[].text` | string | no | The brief, as it was given; present on a single batch. | | `data[].suggestion_count` | integer | yes | How many suggestions Heimdall proposed; 0 until the reading is done. | | `data[].pending_count` | integer | no | How many suggestions still wait for a decision; present on a single batch. | | `data[].provider` | string \| null | yes | The provider that answered, by its configuration key; null until the reading is done. | | `data[].model` | string \| null | yes | The model that answered; null until the reading is done. | | `data[].usage_id` | string \| null | yes | The ledger row that filed the call's cost; null until the reading is done or when the row is gone. | | `data[].failure_reason` | string \| null | yes | Why the reading failed, as a person reads it; null otherwise. | | `data[].created_by` | string \| null | yes | The person who started it; null when the account is gone. | | `data[].creator_name` | string \| null | yes | That person's name, when the creator was loaded; null otherwise. | | `data[].started_at` | string \| null | yes | When Heimdall began reading, ISO 8601; null while queued. | | `data[].finished_at` | string \| null | yes | When the reading ended, well or badly, or the batch was discarded, ISO 8601; null meanwhile. | | `data[].created_at` | string \| null | yes | When the batch was started, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the row last changed, ISO 8601. | | `data[].suggestions` | array of array of any | no | The suggestions, in order; present on a single batch. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the project is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List an import's suggestions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-an-imports-suggestions `GET /v1/imports/{batch}/suggestions` ```bash curl https://api.example.test/v1/imports/$BATCH_ID/suggestions \ -H "Authorization: Bearer $TOKEN" ``` The suggestions of one batch in the order Heimdall proposed them, with the same fields as on the batch. A story's `parent_id` names the epic suggestion it hangs under; accept the epic first and the story lands under the epic it wrote. - Requires ability: `import:view` - MCP tools: `get_council_session`, `get_import_batch` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `batch` | path | string (uuid) | yes | The batch, bound by the middleware. | ### Responses - **200**: Array of `SuggestionResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The suggestion's id. | | `data[].project_id` | string | yes | The project the brief was imported into. | | `data[].batch_id` | string \| null | yes | The import batch it came from; null for a note from a research sweep. | | `data[].kind` | string | yes | What it proposes: `epic`, `story`, `functional_requirement`, `non_functional_requirement`, `business_rule`, `use_case`, `glossary_term`, `persona`, `stakeholder`, `risk`, `assumption`, `decision`, `improvement` or `research`. | | `data[].position` | integer | yes | Its place in the batch, from one, in the order Heimdall proposed. | | `data[].parent_id` | string \| null | yes | The suggestion it hangs under, such as a story's epic; null at the top. | | `data[].title` | string | yes | Its name, from the payload field its kind names: `title`, `name` or `term`. | | `data[].payload` | array \| null | yes | The fields of the record it would become, keyed as the record's own editor names them; the edited fields once accepted with edits. | | `data[].confidence` | number \| null | yes | How directly the brief supports it, 0 to 1; null when the model gave none. | | `data[].rationale` | string \| null | yes | One sentence on why Heimdall proposed it. | | `data[].source_quote` | string \| null | yes | The passage of the brief it drew on, word for word; null for an improvement. | | `data[].source_span` | array \| null | yes | Where that passage sits in the brief, `{ "start": …, "end": … }` as character offsets; null when the quote was not found. | | `data[].sources` | array \| null | yes | The sources a research note cites; null otherwise. | | `data[].voice` | string \| null | yes | The voice of the council that proposed it (`odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`); null for a brief import's or a research run's suggestion. | | `data[].status` | string | yes | What was decided: `pending`, `accepted`, `edited`, `merged`, `rejected` or `dismissed`. | | `data[].duplicate_type` | string \| null | yes | The alias of the record it may duplicate, or was merged into: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `glossary-term`, `persona`, `stakeholder`, `raid-item` or `decision`; null when none. | | `data[].duplicate_id` | string \| null | yes | That record's id; null when none. | | `data[].created_type` | string \| null | yes | The alias of the record the decision wrote; null until accepted, and for a note. | | `data[].created_id` | string \| null | yes | That record's id; null until accepted. | | `data[].decided_by` | string \| null | yes | The person who decided; null while pending or when the account is gone. | | `data[].decider_name` | string \| null | yes | That person's name, when the decider was loaded; null otherwise. | | `data[].decided_at` | string \| null | yes | When it was decided, ISO 8601; null while pending. | | `data[].created_at` | string \| null | yes | When Heimdall proposed it, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the batch is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Merge a suggestion Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/merge-a-suggestion `POST /v1/suggestions/{suggestion}/merge` ```bash curl -X POST https://api.example.test/v1/suggestions/$SUGGESTION_ID/merge \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "artefact_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" }' ``` Points a suggestion at a record of the same kind the project already holds, named by `artefact_id`, leaving the record as it is; the suggestion is marked `merged` with the record under `duplicate_type` and `duplicate_id`, `import.suggestion_merged` goes out and a batch with no pending suggestion left is marked `accepted`. Heimdall's own duplicate candidate, when it named one, is on the suggestion before the merge. - Requires ability: `import:update` - Fires events: `import.suggestion_merged` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `suggestion` | path | string (uuid) | yes | The suggestion, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) Merge a suggestion into a record of the same kind the project already holds: the record is left as it is and the suggestion points at it. | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_id` | string (uuid) | yes | The id of the existing record, of the kind the suggestion proposes and in the same project. | ### Responses - **200**: The suggestion, decided. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The suggestion's id. | | `data.project_id` | string | yes | The project the brief was imported into. | | `data.batch_id` | string \| null | yes | The import batch it came from; null for a note from a research sweep. | | `data.kind` | string | yes | What it proposes: `epic`, `story`, `functional_requirement`, `non_functional_requirement`, `business_rule`, `use_case`, `glossary_term`, `persona`, `stakeholder`, `risk`, `assumption`, `decision`, `improvement` or `research`. | | `data.position` | integer | yes | Its place in the batch, from one, in the order Heimdall proposed. | | `data.parent_id` | string \| null | yes | The suggestion it hangs under, such as a story's epic; null at the top. | | `data.title` | string | yes | Its name, from the payload field its kind names: `title`, `name` or `term`. | | `data.payload` | array \| null | yes | The fields of the record it would become, keyed as the record's own editor names them; the edited fields once accepted with edits. | | `data.confidence` | number \| null | yes | How directly the brief supports it, 0 to 1; null when the model gave none. | | `data.rationale` | string \| null | yes | One sentence on why Heimdall proposed it. | | `data.source_quote` | string \| null | yes | The passage of the brief it drew on, word for word; null for an improvement. | | `data.source_span` | array \| null | yes | Where that passage sits in the brief, `{ "start": …, "end": … }` as character offsets; null when the quote was not found. | | `data.sources` | array \| null | yes | The sources a research note cites; null otherwise. | | `data.voice` | string \| null | yes | The voice of the council that proposed it (`odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`); null for a brief import's or a research run's suggestion. | | `data.status` | string | yes | What was decided: `pending`, `accepted`, `edited`, `merged`, `rejected` or `dismissed`. | | `data.duplicate_type` | string \| null | yes | The alias of the record it may duplicate, or was merged into: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `glossary-term`, `persona`, `stakeholder`, `raid-item` or `decision`; null when none. | | `data.duplicate_id` | string \| null | yes | That record's id; null when none. | | `data.created_type` | string \| null | yes | The alias of the record the decision wrote; null until accepted, and for a note. | | `data.created_id` | string \| null | yes | That record's id; null until accepted. | | `data.decided_by` | string \| null | yes | The person who decided; null while pending or when the account is gone. | | `data.decider_name` | string \| null | yes | That person's name, when the decider was loaded; null otherwise. | | `data.decided_at` | string \| null | yes | When it was decided, ISO 8601; null while pending. | | `data.created_at` | string \| null | yes | When Heimdall proposed it, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the suggestion is hidden from the token or missing, or the record is hidden, missing, of another kind or of another project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the suggestion was already decided, its batch is not ready, or `artefact_id` is not a uuid. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Reject a suggestion Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/reject-a-suggestion `POST /v1/suggestions/{suggestion}/reject` ```bash curl -X POST https://api.example.test/v1/suggestions/$SUGGESTION_ID/reject \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Marks a suggestion rejected, or an improvement dismissed, without writing anything to the project; `import.suggestion_rejected` goes out and a batch with no pending suggestion left is marked `accepted`. - Requires ability: `import:update` - Fires events: `import.suggestion_rejected` - MCP tools: `decide_suggestion` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `suggestion` | path | string (uuid) | yes | The suggestion, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The suggestion, decided. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The suggestion's id. | | `data.project_id` | string | yes | The project the brief was imported into. | | `data.batch_id` | string \| null | yes | The import batch it came from; null for a note from a research sweep. | | `data.kind` | string | yes | What it proposes: `epic`, `story`, `functional_requirement`, `non_functional_requirement`, `business_rule`, `use_case`, `glossary_term`, `persona`, `stakeholder`, `risk`, `assumption`, `decision`, `improvement` or `research`. | | `data.position` | integer | yes | Its place in the batch, from one, in the order Heimdall proposed. | | `data.parent_id` | string \| null | yes | The suggestion it hangs under, such as a story's epic; null at the top. | | `data.title` | string | yes | Its name, from the payload field its kind names: `title`, `name` or `term`. | | `data.payload` | array \| null | yes | The fields of the record it would become, keyed as the record's own editor names them; the edited fields once accepted with edits. | | `data.confidence` | number \| null | yes | How directly the brief supports it, 0 to 1; null when the model gave none. | | `data.rationale` | string \| null | yes | One sentence on why Heimdall proposed it. | | `data.source_quote` | string \| null | yes | The passage of the brief it drew on, word for word; null for an improvement. | | `data.source_span` | array \| null | yes | Where that passage sits in the brief, `{ "start": …, "end": … }` as character offsets; null when the quote was not found. | | `data.sources` | array \| null | yes | The sources a research note cites; null otherwise. | | `data.voice` | string \| null | yes | The voice of the council that proposed it (`odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`); null for a brief import's or a research run's suggestion. | | `data.status` | string | yes | What was decided: `pending`, `accepted`, `edited`, `merged`, `rejected` or `dismissed`. | | `data.duplicate_type` | string \| null | yes | The alias of the record it may duplicate, or was merged into: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `glossary-term`, `persona`, `stakeholder`, `raid-item` or `decision`; null when none. | | `data.duplicate_id` | string \| null | yes | That record's id; null when none. | | `data.created_type` | string \| null | yes | The alias of the record the decision wrote; null until accepted, and for a note. | | `data.created_id` | string \| null | yes | That record's id; null until accepted. | | `data.decided_by` | string \| null | yes | The person who decided; null while pending or when the account is gone. | | `data.decider_name` | string \| null | yes | That person's name, when the decider was loaded; null otherwise. | | `data.decided_at` | string \| null | yes | When it was decided, ISO 8601; null while pending. | | `data.created_at` | string \| null | yes | When Heimdall proposed it, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the suggestion is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the suggestion was already decided or its batch is not ready. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Improvements API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/improvements **Improvements** are Heimdall's standing suggestions on a project's own record: the gaps a good analyst fills before anyone builds. The sweep reads the record, calls no model and spends nothing; it looks for a non-functional requirement without a measure, a story without a negative acceptance criterion, a business rule without a worked example, a data entity without an owner and an open risk without a mitigation, and writes one improvement card per new gap on the review queue, titled with the record's reference. A card a person kept or dismissed is never proposed again, and a pending card whose gap was filled since comes down on the next sweep. The same sweep runs weekly on every live project whose assists are on, and once more when a brief import is fully decided. Tokens run the sweep as staff with `import:create` on a project whose assists are on; the cards are read through the [project's suggestions](https://alm-docs.labs.zuselab.dev/api/v1/reference/research) and decided through the [suggestion endpoints](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports); a client guest's token is refused. ## Endpoints - `POST /v1/projects/{project}/improvements` — [Sweep a project's record](https://alm-docs.labs.zuselab.dev/api/v1/reference/improvements/sweep-a-projects-record) ## Related - [Research API](https://alm-docs.labs.zuselab.dev/api/v1/reference/research): The suggestions endpoint that lists the sweep's cards beside the research notes. - [Imports API](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports): The endpoints that keep or dismiss a card. - [improvement.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/improvement): Heimdall's improvement sweep of a project's record: one event each time it ran, with what it proposed, took down and left waiting. --- # Sweep a project's record Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/improvements/sweep-a-projects-record `POST /v1/projects/{project}/improvements` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/improvements \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Runs the sweep now and answers with what it did: the gaps that became new cards, the pending cards taken down because their gap was filled, and the sweep cards waiting for a decision afterwards. `improvement.swept` goes out with the same counts. - Requires ability: `import:create` - Fires events: `improvement.swept` - MCP tools: `sweep_improvements` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: What the sweep proposed, retired and left pending. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.proposed` | integer | yes | How many gaps became new improvement cards. | | `data.retired` | integer | yes | How many pending cards were taken down because their gap was filled since. | | `data.pending` | integer | yes | How many sweep cards wait for a decision after this sweep. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, the holder's role on the project does not carry `import:create`, or the project's assists are off. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the project is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # API Reference Source: https://alm-docs.labs.zuselab.dev/api/v1/reference Every operation of the REST API, generated from the OpenAPI document Zuse Bifrost publishes and grouped by resource: the request fields with their types and constraints, the response fields, every refusal, the ability the token must carry, the webhook events the call fires, the MCP tools that wrap it, code samples in seven languages, and a playground that sends the request to the API host with your token. Each group has an overview page and one page per operation; a row below opens the operation's page. ## Activity API Every mutation Zuse Bifrost performs, whether from the app, the REST API, an MCP tool call, an accepted Heimdall draft or a scheduled system task, is recorded against its subject in the activity log. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity). - `GET /v1/activity` — [Read a subject's activity](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity/read-a-subjects-activity) ## Alert Destinations API An alert destination is a place your alerts go: your email address or the Telegram chat linked to your account. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations). - `GET /v1/me/alert-destinations` — [List your alert destinations](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations/list-your-alert-destinations) - `PUT /v1/me/alert-destinations` — [Replace your alert destinations](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations/replace-your-alert-destinations) ## Approvals API An approval request is one time a planning record was put up for approval: an epic, a story, a requirement, a business rule or a use case, a baseline or a change request, submitted by its author and decided by the approvers the project's policy names for that kind of approval. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals). - `GET /v1/approvals` — [List the inbox](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-the-inbox) - `GET /v1/approvals/{approval}` — [Get an approval request](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/get-an-approval-request) - `GET /v1/projects/{project}/approval-policies` — [List a project's approval policies](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-a-projects-approval-policies) - `PUT /v1/projects/{project}/approval-policies` — [Update a project's approval policy](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/update-a-projects-approval-policy) - `GET /v1/projects/{project}/approvals` — [List a project's approvals](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-a-projects-approvals) - `POST /v1/approvals/{approval}/decide` — [Decide an approval](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/decide-an-approval) - `POST /v1/approvals/{approval}/withdraw` — [Withdraw an approval](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/withdraw-an-approval) - `POST /v1/baselines/{baseline}/submit` — [Submit a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-baseline) - `POST /v1/change-requests/{changeRequest}/submit` — [Submit a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-change-request) - `POST /v1/business-rules/{rule}/submit` — [Submit a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-business-rule) - `POST /v1/requirements/{requirement}/submit` — [Submit a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-requirement) - `POST /v1/use-cases/{useCase}/submit` — [Submit a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-use-case) - `POST /v1/epics/{epic}/submit` — [Submit an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-an-epic) - `POST /v1/work-items/{item}/submit` — [Submit a story](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-story) ## Attachments API An attachment is a file pinned on one artefact of a project: a wireframe on a story, a signed specification on a requirement, a sample export on a data entity, a vendor quote on a decision. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments). - `GET /v1/projects/{project}/attachments` — [List an artefact's attachments](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/list-an-artefacts-attachments) - `POST /v1/projects/{project}/attachments` — [Upload an attachment](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/upload-an-attachment) - `GET /v1/attachments/{attachment}/download` — [Download an attachment](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/download-an-attachment) - `DELETE /v1/attachments/{attachment}` — [Delete an attachment](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/delete-an-attachment) ## Baselines API A baseline is a named set of approved records frozen together: the scope the client signs before the build starts, what a release ships, what a sprint commits to, or the record as it stood at a milestone. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines). - `GET /v1/baselines/{baseline}` — [Get a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-baseline) - `PATCH /v1/baselines/{baseline}` — [Update a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/update-a-baseline) - `DELETE /v1/baselines/{baseline}` — [Delete a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/delete-a-baseline) - `GET /v1/baselines/{baseline}/revisions` — [List a baseline's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-a-baselines-versions) - `GET /v1/baselines/{baseline}/revisions/{revision}` — [Get a version of a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-version-of-a-baseline) - `GET /v1/projects/{project}/baselines` — [List baselines](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-baselines) - `POST /v1/projects/{project}/baselines` — [Add a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/add-a-baseline) - `PUT /v1/baselines/{baseline}/items` — [Set a baseline's items](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/set-a-baselines-items) - `POST /v1/baselines/{baseline}/status` — [Move a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/move-a-baseline) - `POST /v1/baselines/{baseline}/revisions/{revision}/restore` — [Restore a version of a baseline](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/restore-a-version-of-a-baseline) ## Business Rules API A business rule is a decision of the business the system must honour: a limit it refuses to cross, a fact derived from others, a number worked out from others, a conclusion drawn when conditions hold, an action it takes when they do, or a standing policy. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules). - `GET /v1/business-rules/{rule}` — [Get a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-business-rule) - `PATCH /v1/business-rules/{rule}` — [Update a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/update-a-business-rule) - `DELETE /v1/business-rules/{rule}` — [Delete a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/delete-a-business-rule) - `GET /v1/business-rules/{rule}/revisions` — [List a business rule's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-versions) - `GET /v1/business-rules/{rule}/revisions/{revision}` — [Get a version of a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-version-of-a-business-rule) - `GET /v1/business-rules/{rule}/quality-reviews` — [List a business rule's quality reviews](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-quality-reviews) - `POST /v1/business-rules/{rule}/quality-reviews` — [Review a business rule's quality](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/review-a-business-rules-quality) - `GET /v1/projects/{project}/business-rules` — [List business rules](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-business-rules) - `POST /v1/projects/{project}/business-rules` — [Add a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/add-a-business-rule) - `POST /v1/business-rules/{rule}/status` — [Move a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/move-a-business-rule) - `POST /v1/business-rules/{rule}/revisions/{revision}/restore` — [Restore a version of a business rule](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/restore-a-version-of-a-business-rule) ## Change Requests API A change request is a change asked for after the record was agreed: a new requirement, a clarification, a defect in the baseline, a regulatory demand, a client's wish, a piece of technical debt or a scope reduction. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests). - `GET /v1/change-requests/{changeRequest}` — [Get a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-change-request) - `PATCH /v1/change-requests/{changeRequest}` — [Update a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/update-a-change-request) - `DELETE /v1/change-requests/{changeRequest}` — [Delete a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/delete-a-change-request) - `GET /v1/change-requests/{changeRequest}/revisions` — [List a change request's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-a-change-requests-versions) - `GET /v1/change-requests/{changeRequest}/revisions/{revision}` — [Get a version of a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-version-of-a-change-request) - `GET /v1/projects/{project}/change-requests` — [List change requests](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-change-requests) - `POST /v1/projects/{project}/change-requests` — [Raise a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/raise-a-change-request) - `POST /v1/change-requests/{changeRequest}/status` — [Move a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/move-a-change-request) - `POST /v1/change-requests/{changeRequest}/verify` — [Verify a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/verify-a-change-request) - `POST /v1/change-requests/{changeRequest}/analyse` — [Analyse a change request's impact](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/analyse-a-change-requests-impact) - `POST /v1/change-requests/{changeRequest}/revisions/{revision}/restore` — [Restore a version of a change request](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/restore-a-version-of-a-change-request) ## Clients API A client is an organisation the team does projects for: its names, its primary contact, where it works from and where the relationship stands. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients). - `GET /v1/clients` — [List clients](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/list-clients) - `POST /v1/clients` — [Create a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/create-a-client) - `GET /v1/clients/{client}` — [Get a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/get-a-client) - `PATCH /v1/clients/{client}` — [Update a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/update-a-client) - `DELETE /v1/clients/{client}` — [Delete a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/delete-a-client) - `POST /v1/clients/{client}/archive` — [Archive a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/archive-a-client) - `POST /v1/clients/{client}/restore` — [Restore a client](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/restore-a-client) ## Comments API A comment is a note on one artefact of a project: a question on a requirement, an answer under it, a finding on a story, a decision taken in the margin of a RAID item. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments). - `GET /v1/projects/{project}/comments` — [List an artefact's comments](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/list-an-artefacts-comments) - `POST /v1/projects/{project}/comments` — [Post a comment](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/post-a-comment) - `PATCH /v1/comments/{comment}` — [Edit a comment](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/edit-a-comment) - `DELETE /v1/comments/{comment}` — [Delete a comment](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/delete-a-comment) - `POST /v1/comments/{comment}/resolve` — [Resolve a thread](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/resolve-a-thread) - `POST /v1/comments/{comment}/reopen` — [Reopen a thread](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/reopen-a-thread) ## Components API A component is one application a project is made of: the web app, the API, a mobile app, a background service, an integration, a database or the platform itself. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/components). - `GET /v1/components/{component}` — [Get a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/get-a-component) - `PATCH /v1/components/{component}` — [Update a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/update-a-component) - `DELETE /v1/components/{component}` — [Delete a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/delete-a-component) - `GET /v1/projects/{project}/components` — [List components](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/list-components) - `POST /v1/projects/{project}/components` — [Create a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/create-a-component) - `POST /v1/components/{component}/archive` — [Archive a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/archive-a-component) - `POST /v1/components/{component}/restore` — [Restore a component](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/restore-a-component) ## Council API The council is Heimdall with named voices beside it: Odin for architecture and the data model, Thor for testing, Loki for security, Týr for rigour and compliance, Frigg for planning and risk, Baldur for the people who will use the product and Hel for what fails in production. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/council). - `GET /v1/projects/{project}/council` — [List a project's council sessions](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/list-a-projects-council-sessions) - `POST /v1/projects/{project}/council` — [Convene the council](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/convene-the-council) - `GET /v1/council/{session}` — [Retrieve a council session](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/retrieve-a-council-session) - `POST /v1/council-findings/{finding}/accept` — [Accept a council finding](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/accept-a-council-finding) - `POST /v1/council-findings/{finding}/dismiss` — [Dismiss a council finding](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/dismiss-a-council-finding) ## Data Entities API A data entity is a thing the project keeps records of: a customer, an order, a branch, a shipment. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities). - `GET /v1/data-entities/{entity}` — [Get a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/get-a-data-entity) - `PATCH /v1/data-entities/{entity}` — [Update a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/update-a-data-entity) - `DELETE /v1/data-entities/{entity}` — [Delete a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/delete-a-data-entity) - `GET /v1/data-entities/{entity}/revisions` — [List a entity's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-a-entitys-versions) - `GET /v1/data-entities/{entity}/revisions/{revision}` — [Read one version of a entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/read-one-version-of-a-entity) - `GET /v1/projects/{project}/data-entities` — [List data entities](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-data-entities) - `POST /v1/projects/{project}/data-entities` — [Create a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/create-a-data-entity) - `POST /v1/data-entities/{entity}/status` — [Move a data entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/move-a-data-entity) - `PUT /v1/data-entities/{entity}/fields` — [Replace a data entity's fields](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/replace-a-data-entitys-fields) - `POST /v1/data-entities/{entity}/revisions/{revision}/restore` — [Restore a version of a entity](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/restore-a-version-of-a-entity) ## Decisions API A decision is the record of a choice the project made: the context it was taken in, the options weighed with their pros and cons, what was decided, what follows, who decided and when, and the earlier decision it replaces. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions). - `GET /v1/decisions/{decision}` — [Get a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-decision) - `PATCH /v1/decisions/{decision}` — [Update a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/update-a-decision) - `DELETE /v1/decisions/{decision}` — [Delete a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/delete-a-decision) - `GET /v1/decisions/{decision}/revisions` — [List a decision's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-a-decisions-versions) - `GET /v1/decisions/{decision}/revisions/{revision}` — [Get a version of a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-version-of-a-decision) - `GET /v1/projects/{project}/decisions` — [List decisions](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-decisions) - `POST /v1/projects/{project}/decisions` — [Add a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/add-a-decision) - `POST /v1/decisions/{decision}/status` — [Move a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/move-a-decision) - `POST /v1/decisions/{decision}/revisions/{revision}/restore` — [Restore a version of a decision](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/restore-a-version-of-a-decision) ## Design References API A design reference is a link a project keeps to one of its designs: a Figma file, a wireframe, a prototype, a screenshot, an API contract, a sequence diagram, an entity diagram or a document. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references). - `GET /v1/design-references/{reference}` — [Get a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/get-a-design-reference) - `PATCH /v1/design-references/{reference}` — [Update a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/update-a-design-reference) - `DELETE /v1/design-references/{reference}` — [Delete a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/delete-a-design-reference) - `GET /v1/projects/{project}/design-references` — [List design references](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/list-design-references) - `POST /v1/projects/{project}/design-references` — [Add a design reference](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/add-a-design-reference) ## Environments API An environment is one named place a project's software runs, from a developer's machine to production: its kind, the component it runs (or the whole project), its addresses, the branch deployed there, the autodeploy and protected flags, the health the last check found, the version running there and whether a change freeze is in force. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments). - `GET /v1/environments/{environment}` — [Get an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/get-an-environment) - `PATCH /v1/environments/{environment}` — [Update an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/update-an-environment) - `DELETE /v1/environments/{environment}` — [Delete an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/delete-an-environment) - `GET /v1/projects/{project}/environments` — [List environments](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/list-environments) - `POST /v1/projects/{project}/environments` — [Create an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/create-an-environment) - `POST /v1/environments/{environment}/freeze` — [Freeze an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/freeze-an-environment) - `POST /v1/environments/{environment}/unfreeze` — [Unfreeze an environment](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/unfreeze-an-environment) - `POST /v1/environments/{environment}/health` — [Record an environment's health](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/record-an-environments-health) ## Epics API An epic is a body of work the client recognises: the scope a release is cut from and the parent of every story. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics). - `GET /v1/epics/{epic}` — [Get an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-an-epic) - `PATCH /v1/epics/{epic}` — [Update an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/update-an-epic) - `DELETE /v1/epics/{epic}` — [Delete an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/delete-an-epic) - `GET /v1/epics/{epic}/revisions` — [List an epic's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-an-epics-versions) - `GET /v1/epics/{epic}/revisions/{revision}` — [Get a version of an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-a-version-of-an-epic) - `GET /v1/projects/{project}/epics` — [List epics](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-epics) - `POST /v1/projects/{project}/epics` — [Add an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/add-an-epic) - `POST /v1/epics/{epic}/status` — [Move an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/move-an-epic) - `POST /v1/epics/{epic}/revisions/{revision}/restore` — [Restore a version of an epic](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/restore-a-version-of-an-epic) ## Glossary API A glossary term is a word the project agrees on. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary). - `GET /v1/glossary/{term}` — [Get a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/get-a-glossary-term) - `PATCH /v1/glossary/{term}` — [Update a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/update-a-glossary-term) - `DELETE /v1/glossary/{term}` — [Delete a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/delete-a-glossary-term) - `GET /v1/glossary/{term}/revisions` — [List a term's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-a-terms-versions) - `GET /v1/glossary/{term}/revisions/{revision}` — [Read one version of a term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/read-one-version-of-a-term) - `GET /v1/projects/{project}/glossary` — [List glossary terms](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-glossary-terms) - `POST /v1/projects/{project}/glossary` — [Add a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/add-a-glossary-term) - `POST /v1/glossary/{term}/status` — [Move a glossary term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/move-a-glossary-term) - `POST /v1/glossary/{term}/revisions/{revision}/restore` — [Restore a version of a term](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/restore-a-version-of-a-term) ## Groups API A group is a named bundle of permissions on a team that several members hold at once: put the three people who run the on-call rota into an incident-commanders group and they share its permissions, take one out and they keep only what their own role gives them. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups). - `GET /v1/groups` — [List groups](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/list-groups) - `POST /v1/groups` — [Create a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/create-a-group) - `GET /v1/groups/{group}` — [Get a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/get-a-group) - `PATCH /v1/groups/{group}` — [Update a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/update-a-group) - `DELETE /v1/groups/{group}` — [Delete a group](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/delete-a-group) - `PUT /v1/groups/{group}/members` — [Replace a group's members](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/replace-a-groups-members) ## Imports API A brief import hands a client's brief to Heimdall and gets back suggestions: the epics, stories, requirements, business rules, use cases, glossary terms, personas, stakeholders, risks, assumptions and decisions a good analyst would draft from it, each with how sure Heimdall was, why, and the passage of the brief it drew on, plus improvements, the gaps and questions a project of this kind leaves open. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports). - `GET /v1/projects/{project}/imports` — [List a project's imports](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-a-projects-imports) - `POST /v1/projects/{project}/imports` — [Import a brief](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/import-a-brief) - `GET /v1/imports/{batch}` — [Get an import](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/get-an-import) - `GET /v1/imports/{batch}/suggestions` — [List an import's suggestions](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-an-imports-suggestions) - `POST /v1/imports/{batch}/discard` — [Discard an import](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/discard-an-import) - `POST /v1/suggestions/{suggestion}/accept` — [Accept a suggestion](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/accept-a-suggestion) - `POST /v1/suggestions/{suggestion}/reject` — [Reject a suggestion](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/reject-a-suggestion) - `POST /v1/suggestions/{suggestion}/merge` — [Merge a suggestion](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/merge-a-suggestion) ## Improvements API Improvements are Heimdall's standing suggestions on a project's own record: the gaps a good analyst fills before anyone builds. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/improvements). - `POST /v1/projects/{project}/improvements` — [Sweep a project's record](https://alm-docs.labs.zuselab.dev/api/v1/reference/improvements/sweep-a-projects-record) ## Labels API A label is a word a project pins on its work items: payments, tech-debt, client-asked. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels). - `GET /v1/projects/{project}/labels` — [List a project's labels](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/list-a-projects-labels) - `POST /v1/projects/{project}/labels` — [Add a label](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/add-a-label) - `PATCH /v1/labels/{label}` — [Rewrite a label](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/rewrite-a-label) - `DELETE /v1/labels/{label}` — [Remove a label](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/remove-a-label) ## Links API A link joins two artefacts of a project with a kind that says how the source relates to the target: a story implements a requirement, a test case verifies a rule, a bug is fixed by a story, a decision supersedes an older one. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/links). - `GET /v1/projects/{project}/links` — [List an artefact's links](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/list-an-artefacts-links) - `POST /v1/projects/{project}/links` — [Link two artefacts](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/link-two-artefacts) - `DELETE /v1/links/{link}` — [Unlink two artefacts](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/unlink-two-artefacts) ## Me API GET /v1/me is the first call a client makes. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/me). - `GET /v1/me` — [Get the caller's account](https://alm-docs.labs.zuselab.dev/api/v1/reference/me/get-the-callers-account) ## Notifications API Every alert Zuse Bifrost sends a person (an approval waiting on them, an assignment, a mention, a failing test run, a release approved, an incident opened, a security notice, the Monday digest) also lands in the Notifications Center: the sidebar entry with its unread count and popover, and the page at /notifications. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications). - `GET /v1/me/notifications` — [List the caller's notifications](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/list-the-callers-notifications) - `POST /v1/me/notifications/read-all` — [Mark every notification read](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-every-notification-read) - `POST /v1/me/notifications/{notification}/read` — [Mark a notification read](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-a-notification-read) ## Personas API A persona is an archetypal user the project is built for: a named, sketched person with goals, frustrations and habits, so the analyst, the developers and the testers picture the same someone. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas). - `GET /v1/personas/{persona}` — [Get a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/get-a-persona) - `PATCH /v1/personas/{persona}` — [Update a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/update-a-persona) - `DELETE /v1/personas/{persona}` — [Delete a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/delete-a-persona) - `GET /v1/personas/{persona}/revisions` — [List a persona's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-a-personas-versions) - `GET /v1/personas/{persona}/revisions/{revision}` — [Read one version of a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/read-one-version-of-a-persona) - `GET /v1/projects/{project}/personas` — [List personas](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-personas) - `POST /v1/projects/{project}/personas` — [Create a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/create-a-persona) - `POST /v1/personas/{persona}/archive` — [Archive a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/archive-a-persona) - `POST /v1/personas/{persona}/restore` — [Restore a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/restore-a-persona) - `POST /v1/personas/{persona}/revisions/{revision}/restore` — [Restore a version of a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/restore-a-version-of-a-persona) ## Ping API The one endpoint that needs no token. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/ping). - `GET /v1/ping` — [Check the API is up](https://alm-docs.labs.zuselab.dev/api/v1/reference/ping/check-the-api-is-up) ## Project Settings API A project's settings are its per-project knobs: how long a sprint runs and on which days, which timezone its deadlines are read in, what work is sized in and on which scale, who a new item goes to, whether pull requests move items along and whether checklists gate a sprint or a done, whether a client guest may comment, whether Heimdall assists, where alerts go on Telegram, the board's limits and hidden columns, the business hours SLAs count and whether a production outage opens an incident. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings). - `GET /v1/projects/{project}/settings` — [Get a project's settings](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/get-a-projects-settings) - `PATCH /v1/projects/{project}/settings` — [Update a project's settings](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/update-a-projects-settings) ## Projects API A project is one client engagement from discovery to maintenance: the container every component, environment, epic, story, requirement, test, release and incident lives under. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects). - `GET /v1/projects` — [List projects](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/list-projects) - `POST /v1/projects` — [Create a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/create-a-project) - `GET /v1/projects/{project}` — [Get a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/get-a-project) - `PATCH /v1/projects/{project}` — [Update a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/update-a-project) - `DELETE /v1/projects/{project}` — [Delete a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/delete-a-project) - `POST /v1/projects/{project}/archive` — [Archive a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/archive-a-project) - `POST /v1/projects/{project}/restore` — [Restore a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/restore-a-project) ## RAID Log API The RAID log is where a project writes down what may go wrong and what it leans on: risks (something that may happen and would hurt, scored by probability and impact, with a mitigation and a contingency), assumptions (something taken as true for the plan to hold, reviewed until it is confirmed or fails), issues (something that has happened and hurts now, scored by impact) and dependencies (something the project needs from outside itself). [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log). - `GET /v1/projects/{project}/raid-items` — [List RAID items](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-raid-items) - `POST /v1/projects/{project}/raid-items` — [Add a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/add-a-raid-item) - `GET /v1/raid-items/{raidItem}` — [Get a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-raid-item) - `PATCH /v1/raid-items/{raidItem}` — [Update a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/update-a-raid-item) - `DELETE /v1/raid-items/{raidItem}` — [Delete a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/delete-a-raid-item) - `GET /v1/raid-items/{raidItem}/revisions` — [List a RAID item's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-a-raid-items-versions) - `GET /v1/raid-items/{raidItem}/revisions/{revision}` — [Get a version of a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-version-of-a-raid-item) - `POST /v1/raid-items/{raidItem}/status` — [Move a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/move-a-raid-item) - `POST /v1/raid-items/{raidItem}/revisions/{revision}/restore` — [Restore a version of a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/restore-a-version-of-a-raid-item) ## Repositories API A repository is a Git repository a project's code lives in: its provider, the account that owns it, its name, the branch releases are cut from, whether it is private and which component it builds. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories). - `GET /v1/repositories/{repository}` — [Get a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/get-a-repository) - `PATCH /v1/repositories/{repository}` — [Update a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/update-a-repository) - `DELETE /v1/repositories/{repository}` — [Delete a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/delete-a-repository) - `GET /v1/projects/{project}/repositories` — [List repositories](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/list-repositories) - `POST /v1/projects/{project}/repositories` — [Register a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/register-a-repository) - `POST /v1/repositories/{repository}/disconnect` — [Disconnect a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/disconnect-a-repository) - `POST /v1/repositories/{repository}/reconnect` — [Reconnect a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/reconnect-a-repository) ## Requirements API A requirement is one sentence the client and the team agree the system must honour: a functional requirement says what it does for a person (CRM-FR-12), a non-functional one says a quality it keeps while it does it (CRM-NFR-3), with the category the quality belongs to and a measure with the metric and the target. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements). - `GET /v1/requirements/{requirement}` — [Get a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-requirement) - `PATCH /v1/requirements/{requirement}` — [Update a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/update-a-requirement) - `DELETE /v1/requirements/{requirement}` — [Delete a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/delete-a-requirement) - `GET /v1/requirements/{requirement}/revisions` — [List a requirement's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-versions) - `GET /v1/requirements/{requirement}/revisions/{revision}` — [Get a version of a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-version-of-a-requirement) - `GET /v1/requirements/{requirement}/quality-reviews` — [List a requirement's quality reviews](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-quality-reviews) - `POST /v1/requirements/{requirement}/quality-reviews` — [Review a requirement's quality](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/review-a-requirements-quality) - `GET /v1/projects/{project}/requirements` — [List requirements](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-requirements) - `POST /v1/projects/{project}/requirements` — [Add a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/add-a-requirement) - `POST /v1/requirements/{requirement}/status` — [Move a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/move-a-requirement) - `POST /v1/requirements/{requirement}/revisions/{revision}/restore` — [Restore a version of a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/restore-a-version-of-a-requirement) ## Research API Research is Heimdall reading the web for what a project's brief never states: what products of this kind commonly do, the laws, regulations and standards that apply in the client's country and industry, the integrations the client will expect, what the market already offers and the security and accessibility expectations the client will be held to. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/research). - `GET /v1/projects/{project}/research` — [List a project's research](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research) - `POST /v1/projects/{project}/research` — [Research a project's domain](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/research-a-projects-domain) - `GET /v1/projects/{project}/suggestions` — [List a project's research suggestions](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research-suggestions) ## Roles API A role is the permission set a member holds on a team. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles). - `GET /v1/roles` — [List roles](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/list-roles) - `POST /v1/roles` — [Create a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/create-a-role) - `GET /v1/roles/{role}` — [Get a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/get-a-role) - `PATCH /v1/roles/{role}` — [Update a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/update-a-role) - `DELETE /v1/roles/{role}` — [Delete a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/delete-a-role) ## Stakeholders API A stakeholder is a person the project answers to: the client's sponsor and product owner, the people who sign off requirements, UAT runs and releases, Zuse's own account manager or architect, and third parties such as an auditor or a vendor. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders). - `GET /v1/projects/{project}/stakeholders` — [List stakeholders](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-stakeholders) - `POST /v1/projects/{project}/stakeholders` — [Add a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/add-a-stakeholder) - `GET /v1/stakeholders/{stakeholder}` — [Get a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/get-a-stakeholder) - `PATCH /v1/stakeholders/{stakeholder}` — [Update a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/update-a-stakeholder) - `DELETE /v1/stakeholders/{stakeholder}` — [Remove a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/remove-a-stakeholder) - `GET /v1/stakeholders/{stakeholder}/revisions` — [List a stakeholder's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-a-stakeholders-versions) - `GET /v1/stakeholders/{stakeholder}/revisions/{revision}` — [Read one version of a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/read-one-version-of-a-stakeholder) - `POST /v1/stakeholders/{stakeholder}/revisions/{revision}/restore` — [Restore a version of a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/restore-a-version-of-a-stakeholder) ## Team Members API Team members are the people inside a team: the owner and everyone invited to work in it, staff and client guests alike. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members). - `GET /v1/teams/{team}/invitations` — [List a team's pending invitations](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/list-a-teams-pending-invitations) - `GET /v1/teams/{team}/members` — [List a team's members](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/list-a-teams-members) - `POST /v1/teams/{team}/members` — [Invite a team member](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/invite-a-team-member) - `GET /v1/teams/{team}/members/{member}` — [Get a team member](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/get-a-team-member) - `DELETE /v1/teams/{team}/members/{member}` — [Remove a team member](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/remove-a-team-member) - `PATCH /v1/teams/{team}/members/{member}/role` — [Change a team member's role](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/change-a-team-members-role) - `DELETE /v1/teams/{team}/invitations/{invitation}` — [Withdraw an invitation](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/withdraw-an-invitation) ## Teams API A team is the tenant: the container every project, role, group, token and webhook endpoint hangs off. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams). - `GET /v1/teams` — [List teams](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/list-teams) - `POST /v1/teams` — [Create a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/create-a-team) - `GET /v1/teams/current` — [Get the current team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-the-current-team) - `GET /v1/teams/{team}` — [Get a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-a-team) - `PATCH /v1/teams/{team}` — [Rename a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/rename-a-team) - `DELETE /v1/teams/{team}` — [Delete a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/delete-a-team) ## Tokens API Personal access tokens are what authorise every REST and MCP call. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens). - `GET /v1/tokens` — [List the caller's tokens](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/list-the-callers-tokens) - `POST /v1/tokens` — [Mint a token](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/mint-a-token) - `GET /v1/tokens/{token}` — [Get a token](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/get-a-token) - `DELETE /v1/tokens/{token}` — [Revoke a token](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/revoke-a-token) ## Use Cases API A use case is how an actor reaches a goal with the system, step by step: the persona who acts, the other actors in the room, the goal, what triggers it, what must be true before and after, how often it happens, and the steps (what the actor does and what the system does in reply) in a main flow with alternate and exception flows branching off numbered main steps. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases). - `GET /v1/projects/{project}/use-cases` — [List use cases](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-use-cases) - `POST /v1/projects/{project}/use-cases` — [Add a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/add-a-use-case) - `GET /v1/use-cases/{useCase}` — [Get a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-use-case) - `PATCH /v1/use-cases/{useCase}` — [Update a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/update-a-use-case) - `DELETE /v1/use-cases/{useCase}` — [Delete a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/delete-a-use-case) - `GET /v1/use-cases/{useCase}/revisions` — [List a use case's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-a-use-cases-versions) - `GET /v1/use-cases/{useCase}/revisions/{revision}` — [Get a version of a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-version-of-a-use-case) - `POST /v1/use-cases/{useCase}/status` — [Move a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/move-a-use-case) - `PUT /v1/use-cases/{useCase}/steps` — [Replace a use case's steps](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/replace-a-use-cases-steps) - `POST /v1/use-cases/{useCase}/revisions/{revision}/restore` — [Restore a version of a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/restore-a-version-of-a-use-case) ## Watchers API A watcher is a person following one artefact: they are told of every comment posted on it. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers). - `GET /v1/projects/{project}/watchers` — [List an artefact's followers](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/list-an-artefacts-followers) - `POST /v1/projects/{project}/watchers` — [Follow an artefact](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/follow-an-artefact) - `DELETE /v1/watchers/{watcher}` — [Stop following](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/stop-following) ## Webhook Deliveries API Every event Zuse Bifrost posts to one of your webhook endpoints is a delivery: one row per endpoint per event, carrying the signed envelope that was sent, the target's response and where the row is on the retry ladder. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries). - `GET /v1/webhook-deliveries` — [List webhook deliveries](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/list-webhook-deliveries) - `GET /v1/webhook-deliveries/{delivery}` — [Get a webhook delivery](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/get-a-webhook-delivery) - `POST /v1/webhook-deliveries/retry-dead` — [Retry every recent dead delivery](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-every-recent-dead-delivery) - `POST /v1/webhook-deliveries/{delivery}/retry` — [Retry a webhook delivery](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-a-webhook-delivery) ## Webhook Endpoints API A webhook endpoint is a URL of yours that Zuse Bifrost posts events to, with the events it subscribes to and a signing secret you verify each delivery against. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints). - `GET /v1/webhook-endpoints` — [List webhook endpoints](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/list-webhook-endpoints) - `POST /v1/webhook-endpoints` — [Register a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/register-a-webhook-endpoint) - `GET /v1/webhook-endpoints/{endpoint}` — [Get a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/get-a-webhook-endpoint) - `DELETE /v1/webhook-endpoints/{endpoint}` — [Delete a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/delete-a-webhook-endpoint) - `POST /v1/webhook-endpoints/{endpoint}/pause` — [Pause a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/pause-a-webhook-endpoint) - `POST /v1/webhook-endpoints/{endpoint}/resume` — [Resume a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/resume-a-webhook-endpoint) - `POST /v1/webhook-endpoints/{endpoint}/rotate-secret` — [Rotate a webhook endpoint's secret](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/rotate-a-webhook-endpoints-secret) - `POST /v1/webhook-endpoints/{endpoint}/test` — [Test-fire a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/test-fire-a-webhook-endpoint) ## Webhook Events API A read-only polling endpoint that surfaces the most recent webhook deliveries for a given event type. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-events). - `GET /v1/webhook-events` — [Poll recent webhook events](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-events/poll-recent-webhook-events) ## Work Items API A work item is one row of the tracker: a story, a task, a sub-task, a bug, a spike or an improvement. [Read the overview](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items). - `GET /v1/me/work-items` — [List my work items](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-my-work-items) - `GET /v1/projects/{project}/work-items` — [List work items](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-work-items) - `POST /v1/projects/{project}/work-items` — [Add a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/add-a-work-item) - `GET /v1/work-items/{item}` — [Get a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-work-item) - `PATCH /v1/work-items/{item}` — [Update a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/update-a-work-item) - `DELETE /v1/work-items/{item}` — [Delete a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/delete-a-work-item) - `GET /v1/work-items/{item}/revisions` — [List a work item's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-versions) - `GET /v1/work-items/{item}/revisions/{revision}` — [Get a version of a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-version-of-a-work-item) - `GET /v1/work-items/{item}/quality-reviews` — [List a work item's quality reviews](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-quality-reviews) - `POST /v1/work-items/{item}/quality-reviews` — [Review a work item's quality](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/review-a-work-items-quality) - `POST /v1/work-items/{item}/status` — [Move a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/move-a-work-item) - `POST /v1/work-items/{item}/assign` — [Assign a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/assign-a-work-item) - `PUT /v1/work-items/{item}/acceptance-criteria` — [Replace a work item's acceptance criteria](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/replace-a-work-items-acceptance-criteria) - `POST /v1/work-items/{item}/revisions/{revision}/restore` — [Restore a version of a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/restore-a-version-of-a-work-item) --- # Add a label Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/add-a-label `POST /v1/projects/{project}/labels` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/labels \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "payments", "color": "amber", "description": "Touches the checkout or the ledger" }' ``` Adds a word to the vocabulary. `name` must be free within the project whatever its case; `color` is one of the palette and `zinc` by default. Answers `201` with the label and emits `label.created`. - Requires ability: `work-item:update` - Fires events: `label.created` - MCP tools: `create_label` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A label to add to the project's vocabulary, or new values for one. On a rewrite, a field left out keeps its value. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The word, 1 to 60 characters, unique within the project whatever its case. Required when adding. | | `color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` | no | The badge colour: `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink` or `rose`. `zinc` by default. | | `description` | string \| null | no | What the label means, up to 160 characters. | ### Responses - **201**: The label, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The label's id. | | `data.project_id` | string | yes | The project whose vocabulary it belongs to. | | `data.name` | string | yes | The word, unique within the project whatever its case. | | `data.color` | string | yes | The badge colour: `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink` or `rose`. | | `data.description` | string \| null | yes | What the label means; null when nothing was written. | | `data.work_item_count` | integer \| null | yes | How many work items carry it, when the listing counted them; null otherwise. | | `data.created_at` | string \| null | yes | When the label was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the label last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the name is missing, over 60 characters or taken, the colour is not in the palette, or the description is over 160 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Labels API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/labels A **label** is a word a project pins on its work items: `payments`, `tech-debt`, `client-asked`. The vocabulary is the project's own, kept short, each word unique within the project whatever its case and coloured so the backlog reads at a glance. Labels are pinned through the work item endpoints' `label_ids` and read back on every item as `labels`; these endpoints shape the vocabulary itself. A label is not an artefact: it carries no versions, and removing it unpins it everywhere without touching the items. Shaping the vocabulary follows the work item permissions: whoever may change a story may add the word it needs (`work-item:update`), and whoever may read the backlog may read the words (`work-item:view-any`). Every write announces itself as a `label.*` event. ## Endpoints - `GET /v1/projects/{project}/labels` — [List a project's labels](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/list-a-projects-labels) - `POST /v1/projects/{project}/labels` — [Add a label](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/add-a-label) - `PATCH /v1/labels/{label}` — [Rewrite a label](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/rewrite-a-label) - `DELETE /v1/labels/{label}` — [Remove a label](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/remove-a-label) ## Related - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items): Where labels are pinned, with label_ids on a write and label_id as a filter. - [label.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label): A project's label vocabulary: a label added, rewritten and removed. --- # List a project's labels Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/list-a-projects-labels `GET /v1/projects/{project}/labels` ```bash curl https://api.example.test/v1/projects/$PROJECT_ID/labels \ -H "Authorization: Bearer $TOKEN" ``` The vocabulary by name, each label counting the work items it is pinned on. - Requires ability: `work-item:view-any` - MCP tools: `list_labels` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | ### Responses - **200**: Array of `LabelResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The label's id. | | `data[].project_id` | string | yes | The project whose vocabulary it belongs to. | | `data[].name` | string | yes | The word, unique within the project whatever its case. | | `data[].color` | string | yes | The badge colour: `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink` or `rose`. | | `data[].description` | string \| null | yes | What the label means; null when nothing was written. | | `data[].work_item_count` | integer \| null | yes | How many work items carry it, when the listing counted them; null otherwise. | | `data[].created_at` | string \| null | yes | When the label was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the label last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Remove a label Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/remove-a-label `DELETE /v1/labels/{label}` ```bash curl -X DELETE https://api.example.test/v1/labels/$LABEL_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes the word from the vocabulary and unpins it from every item; the items are untouched. Returns `204 No Content` and emits `label.deleted`. - Requires ability: `work-item:update` - Fires events: `label.deleted` - MCP tools: `delete_label` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `label` | path | string (uuid) | yes | The label, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the label is of a project the token may not see. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Rewrite a label Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/rewrite-a-label `PATCH /v1/labels/{label}` ```bash curl -X PATCH https://api.example.test/v1/labels/$LABEL_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "color": "rose" }' ``` Changes the word, the colour or the note; a field left out keeps its value, and every item carrying the label shows the new values at once. Emits `label.updated` when something changed. - Requires ability: `work-item:update` - Fires events: `label.updated` - MCP tools: `update_label` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `label` | path | string (uuid) | yes | The label, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A label to add to the project's vocabulary, or new values for one. On a rewrite, a field left out keeps its value. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The word, 1 to 60 characters, unique within the project whatever its case. Required when adding. | | `color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` | no | The badge colour: `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink` or `rose`. `zinc` by default. | | `description` | string \| null | no | What the label means, up to 160 characters. | ### Responses - **200**: The label. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The label's id. | | `data.project_id` | string | yes | The project whose vocabulary it belongs to. | | `data.name` | string | yes | The word, unique within the project whatever its case. | | `data.color` | string | yes | The badge colour: `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink` or `rose`. | | `data.description` | string \| null | yes | What the label means; null when nothing was written. | | `data.work_item_count` | integer \| null | yes | How many work items carry it, when the listing counted them; null otherwise. | | `data.created_at` | string \| null | yes | When the label was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the label last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the label is of a project the token may not see. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the new name is taken by another label, over 60 characters, or the colour is not in the palette. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Links API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/links A **link** joins two artefacts of a project with a kind that says how the source relates to the target: a story `implements` a requirement, a test case `verifies` a rule, a bug `is fixed by` a story, a decision `supersedes` an older one. The link is stored once and read from both sides, so the requirement sees that it `is implemented by` the story. Nine artefact types may be linked today: epics, work items, requirements, business rules, use cases, RAID items, decisions, data entities and glossary terms. The traceability matrix is a query over these links, so the kinds are fixed and named rather than typed in. The links are internal. A staff token sees the links of the projects it may see, with both ends named; a client guest's token sees none of them, and a link it asks for is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the links panel runs, under the project permissions, and announces itself as a `link.*` event. ## Endpoints - `GET /v1/projects/{project}/links` — [List an artefact's links](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/list-an-artefacts-links) - `POST /v1/projects/{project}/links` — [Link two artefacts](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/link-two-artefacts) - `DELETE /v1/links/{link}` — [Unlink two artefacts](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/unlink-two-artefacts) ## Related - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items): [requirements](/api/v1/reference/requirements), [business rules](/api/v1/reference/business-rules), [use cases](/api/v1/reference/use-cases), [the RAID log](/api/v1/reference/raid-log) and [decisions](/api/v1/reference/decisions): the artefacts the links join. - [link.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link): The links between a project's artefacts: a link made and a link removed. --- # Link two artefacts Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/links/link-two-artefacts `POST /v1/projects/{project}/links` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/links \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "source_type": "work-item", "source_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "target_type": "requirement", "target_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "kind": "implements" }' ``` Both ends must be artefacts of the project the token may see, and not the same artefact. The same kind joins two artefacts once; for a kind that reads the same both ways (`relates_to`, `duplicates`) the reverse link counts as the same link. `kind` defaults to `relates_to`. Answers `201` with the link, both ends named, and emits `link.created`. - Requires ability: `link:create` - Fires events: `link.created` - MCP tools: `link_artefacts` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A link to make: the source and the target by type and id, both of the project, and the kind read from the source. The same kind joins two artefacts once; a kind that reads the same both ways (`relates_to`, `duplicates`) counts the reverse link as the same link. | Field | Type | Required | Description | | --- | --- | --- | --- | | `source_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The source artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `source_id` | string (uuid) | yes | The source artefact's id, one of the project's. | | `target_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The target artefact's type, from the same list. | | `target_id` | string (uuid) | yes | The target artefact's id, one of the project's and not the source. | | `kind` | `implements`, `derives_from`, `verifies`, `covers`, `depends_on`, `blocks`, `relates_to`, `duplicates`, `caused_by`, `fixed_by`, `supersedes` \| null | no | How the source relates to the target: `implements`, `derives_from`, `verifies`, `covers`, `depends_on`, `blocks`, `relates_to`, `duplicates`, `caused_by`, `fixed_by` or `supersedes`. Defaults to `relates_to`. | | `note` | string \| null | no | A few words on why, up to 300 characters. | ### Responses - **201**: The link, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The link's id. | | `data.project_id` | string | yes | The project both artefacts belong to. | | `data.source_type` | string | yes | The source artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data.source_id` | string | yes | The source artefact's id. | | `data.source_reference` | string \| null | yes | The source's key (its reference, or its name for the dictionary), when the source was loaded; null otherwise. | | `data.source_title` | string \| null | yes | What the source is called, when it was loaded; null otherwise. | | `data.target_type` | string | yes | The target artefact's type, from the same list. | | `data.target_id` | string | yes | The target artefact's id. | | `data.target_reference` | string \| null | yes | The target's key, when the target was loaded; null otherwise. | | `data.target_title` | string \| null | yes | What the target is called, when it was loaded; null otherwise. | | `data.kind` | string | yes | How the source relates to the target: `implements`, `derives_from`, `verifies`, `covers`, `depends_on`, `blocks`, `relates_to`, `duplicates`, `caused_by`, `fixed_by` or `supersedes`. | | `data.phrase` | `implements`, `derives from`, `verifies`, `covers`, `depends on`, `blocks`, `relates to`, `duplicates`, `is caused by`, `is fixed by`, `supersedes` | yes | The kind read from the source, in words, such as `implements`. | | `data.inverse_phrase` | `is implemented by`, `is refined by`, `is verified by`, `is covered by`, `is depended on by`, `is blocked by`, `relates to`, `duplicates`, `causes`, `fixes`, `is superseded by` | yes | The kind read from the target, in words, such as `is implemented by`. | | `data.note` | string \| null | yes | A few words on why the two are linked; null when none. | | `data.created_by` | string \| null | yes | The person who made the link; null when the account is gone. | | `data.created_at` | string \| null | yes | When the link was made, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `link:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a type or a kind is unknown, an id is not a uuid, an end is hidden, missing or of another project, the two ends are the same artefact, or the link exists already. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List an artefact's links Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/links/list-an-artefacts-links `GET /v1/projects/{project}/links` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/links?artefact_type=work-item&artefact_id=$WORK_ITEM_ID" \ -H "Authorization: Bearer $TOKEN" ``` Every link that starts from or points at one artefact, newest first, each with both ends' types, ids, keys and titles, the kind, the phrase read from the source and the phrase read from the target. `artefact_type` and `artefact_id` are required; the artefact must be one the token may see. - Requires ability: `link:view-any` - MCP tools: `list_links` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `artefact_type` | query | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. Required. | | `artefact_id` | query | string (uuid) | yes | The artefact's id. Required. | ### Responses - **200**: Array of `LinkResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The link's id. | | `data[].project_id` | string | yes | The project both artefacts belong to. | | `data[].source_type` | string | yes | The source artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data[].source_id` | string | yes | The source artefact's id. | | `data[].source_reference` | string \| null | yes | The source's key (its reference, or its name for the dictionary), when the source was loaded; null otherwise. | | `data[].source_title` | string \| null | yes | What the source is called, when it was loaded; null otherwise. | | `data[].target_type` | string | yes | The target artefact's type, from the same list. | | `data[].target_id` | string | yes | The target artefact's id. | | `data[].target_reference` | string \| null | yes | The target's key, when the target was loaded; null otherwise. | | `data[].target_title` | string \| null | yes | What the target is called, when it was loaded; null otherwise. | | `data[].kind` | string | yes | How the source relates to the target: `implements`, `derives_from`, `verifies`, `covers`, `depends_on`, `blocks`, `relates_to`, `duplicates`, `caused_by`, `fixed_by` or `supersedes`. | | `data[].phrase` | `implements`, `derives from`, `verifies`, `covers`, `depends on`, `blocks`, `relates to`, `duplicates`, `is caused by`, `is fixed by`, `supersedes` | yes | The kind read from the source, in words, such as `implements`. | | `data[].inverse_phrase` | `is implemented by`, `is refined by`, `is verified by`, `is covered by`, `is depended on by`, `is blocked by`, `relates to`, `duplicates`, `causes`, `fixes`, `is superseded by` | yes | The kind read from the target, in words, such as `is implemented by`. | | `data[].note` | string \| null | yes | A few words on why the two are linked; null when none. | | `data[].created_by` | string \| null | yes | The person who made the link; null when the account is gone. | | `data[].created_at` | string \| null | yes | When the link was made, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `link:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `artefact_type` is not one of the nine or `artefact_id` is not a uuid. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Unlink two artefacts Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/links/unlink-two-artefacts `DELETE /v1/links/{link}` ```bash curl -X DELETE https://api.example.test/v1/links/$LINK_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes the link; the two artefacts are untouched. Returns `204 No Content` and emits `link.deleted`. - Requires ability: `link:delete` - Fires events: `link.deleted` - MCP tools: `remove_link` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `link` | path | string (uuid) | yes | The link, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `link:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the link is of a project the token may not see, or the token is a client guest's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get the caller's account Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/me/get-the-callers-account `GET /v1/me` ```bash curl https://api.example.test/v1/me \ -H "Authorization: Bearer $TOKEN" ``` The caller's account; never paginated. > **Read it once per session.** Nothing here changes under a running client except by an action the client itself takes (changing alert destinations, linking Telegram, joining a team). Read it on sign-in and after those actions; there is no need to poll it. - `current_team` is the team the token is scoped to (`scope:team:`), when you still belong to it; `null` when the token carries no team scope. Its `id` is the value every team call resolves against. - `teams` is every team you own or are a member of, the same list as [`GET /v1/teams`](/api/v1/reference/teams). - `alert_destinations` is the same list as [`GET /v1/me/alert-destinations`](/api/v1/reference/alert-destinations); `alert_destinations_default` is `true` while no destination has been written and the default routing applies (every class to your linked Telegram chat when there is one, and email for what nothing else carries). - `unread_notifications` is how many entries of your [Notifications Center](/api/v1/reference/notifications) you have not opened, the number on the sidebar entry. - `timezone` is the IANA name every timestamp the app shows you is in; the API itself always answers in UTC with an offset. - Requires ability: `account:read` - MCP tools: `get_me` ### Responses - **200**: The caller's account. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The person's user id. | | `data.name` | string | yes | The person's name. | | `data.email` | string | yes | The person's email address. | | `data.title` | string \| null | yes | The person's job title; null when none was given. | | `data.locale` | string | yes | The person's language, as a locale code. | | `data.timezone` | string | yes | The person's timezone, an IANA name; every timestamp the app shows them is in it. | | `data.email_verified` | boolean | yes | Whether the email address has been verified. | | `data.current_team` | array \| null | yes | The team the token is scoped to, when the person still belongs to it; null when the token carries no team scope. Its id is the value every team call resolves against. | | `data.teams` | array of any | yes | Every team the person owns or is a member of, the same list as the teams endpoint. | | `data.alert_destinations` | array of any | yes | Where each alert class goes, the same list as the alert destinations endpoint. | | `data.alert_destinations_default` | boolean | yes | True while no destination has been written and the default routing applies: every class to the linked Telegram chat when there is one, and email for what nothing else carries. | | `data.unread_notifications` | integer | yes | How many Notifications Center entries the person has not opened, the number on the sidebar entry. | | `data.telegram_linked` | boolean | yes | Whether the account is linked to the Telegram bot. | | `data.created_at` | string \| null | yes | When the account was created, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Me API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/me `GET /v1/me` is the first call a client makes. It answers "who am I acting as?" in one round trip: the person behind the token, the **team the token is scoped to** (the id every team call resolves against), every team they belong to, where their alerts go, how many notifications they have not opened and whether their account is linked to the Telegram bot. The app shows the same facts across **Account Settings → Profile**, **Security** and **Notifications**; a client renders its home screen from this payload. It lives under `/v1/me` with no team or project in the path because it is about the caller, never a project. ## Endpoints - `GET /v1/me` — [Get the caller's account](https://alm-docs.labs.zuselab.dev/api/v1/reference/me/get-the-callers-account) ## Related - [Teams API](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams): GET /v1/teams/current returns current_team alone. - [Alert Destinations API](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations): Change where alerts go. - [Notifications API](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications): The entries behind unread_notifications. --- # Notifications API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications Every alert Zuse Bifrost sends a person (an approval waiting on them, an assignment, a mention, a failing test run, a release approved, an incident opened, a security notice, the Monday digest) also lands in the **Notifications Center**: the sidebar entry with its unread count and popover, and the page at `/notifications`. These endpoints read the same rows and make the same two marks, so a client or an agent can show the inbox and clear it. The rows belong to the **person behind the token**, never to a team or a project, so everything lives under `/v1/me`. `account:read` lists them; `account:write` marks them. ## Endpoints - `GET /v1/me/notifications` — [List the caller's notifications](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/list-the-callers-notifications) - `POST /v1/me/notifications/read-all` — [Mark every notification read](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-every-notification-read) - `POST /v1/me/notifications/{notification}/read` — [Mark a notification read](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-a-notification-read) ## Related - [Me API](https://alm-docs.labs.zuselab.dev/api/v1/reference/me): The caller, with unread_notifications, so a client can draw the sidebar badge without listing the inbox. - [Alert Destinations API](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations): Where each class of alert is delivered. - [Notifications](https://alm-docs.labs.zuselab.dev/account/notifications): The Notifications Center in the app. --- # List the caller's notifications Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/list-the-callers-notifications `GET /v1/me/notifications` ```bash curl "https://api.example.test/v1/me/notifications?unread=1&class=approvals" \ -H "Authorization: Bearer $TOKEN" ``` Newest first, paginated with `page` and `per_page` like every list (15 by default). `unread=1` narrows to entries not yet read (`unread=0` to the ones already read); `class` narrows to one class; `q` to entries whose title or body contains the words. - `class` is the alert class the person routes on **Account Settings → Notifications**: `approvals`, `assignments`, `mentions`, `planning`, `sprints`, `code`, `testing`, `releases`, `deployments`, `incidents`, `support`, `security`, `digest`, `onboarding` or `budget`; `class_label` is its name in the person's language. - `title` and `body` are the alert's heading and one sentence of context, the same words the email used, in the person's language at the time it was sent. - `route` and `params` name the app screen the entry opens, as a named route and its parameters, for a client that maps routes to screens; `url` is the same place resolved, for a client that does not. All three are `null` when the alert points nowhere. - `read` is whether the person opened it; `read_at` says when. Every alert is written to the Notifications Center beside whatever else it sends (the email, the Telegram message the person routed it to). There is no webhook for a new entry; the alerts that matter to an integration already fire their own events. - Requires ability: `account:read` - MCP tools: `list_notifications` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `unread` | query | boolean \| null | no | `1` narrows to entries not yet read, `0` to the ones already read; omit it for both. | | `class` | query | `approvals`, `assignments`, `mentions`, `planning`, `sprints`, `code`, `testing`, `releases`, `deployments`, `incidents`, `support`, `security`, `digest`, `onboarding`, `budget` \| null | no | Narrow to one alert class, such as `approvals` or `incidents`. An unknown class is refused. | | `q` | query | string \| null | no | Words the title or body must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 15. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: The page, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The entry's id, the value the mark-read endpoint takes. | | `data[].class` | string | yes | The alert class the person routes on Account Settings → Notifications: `approvals`, `assignments`, `mentions`, `planning`, `sprints`, `code`, `testing`, `releases`, `deployments`, `incidents`, `support`, `security`, `digest`, `onboarding` or `budget`. | | `data[].class_label` | `Approvals`, `Assignments`, `Mentions`, `Planning`, `Sprints`, `Code`, `Testing`, `Releases`, `Deployments`, `Incidents`, `Support`, `Security`, `Digest`, `Onboarding`, `AI budget` | yes | The class's name in the person's language. | | `data[].title` | string | yes | The alert's heading, the same words the email used, in the person's language at the time it was sent. | | `data[].body` | string | yes | One sentence of context, the email's first line. | | `data[].route` | string \| null | yes | The app screen the entry opens, as a named route, for a client that maps routes to screens; null when the alert points nowhere. | | `data[].params` | object | yes | The named route's parameters; empty when it takes none. | | `data[].url` | string \| null | yes | The same place resolved to a URL, for a client that does not map routes; null when the alert points nowhere. | | `data[].read` | boolean | yes | Whether the person opened the entry. | | `data[].read_at` | string \| null | yes | When it was opened, ISO 8601; null while unread. | | `data[].created_at` | string \| null | yes | When the alert arrived, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `class` is not one of the alert classes. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Mark a notification read Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-a-notification-read `POST /v1/me/notifications/{notification}/read` ```bash curl -X POST https://api.example.test/v1/me/notifications/$NOTIFICATION_ID/read \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Answers `200` with the entry, `read` now `true`. Idempotent: an entry already read keeps its first `read_at`. - Requires ability: `account:write` - MCP tools: `mark_notification_read` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `notification` | path | string | yes | The row, resolved by the route binder as the caller's. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: 200 with the row, read. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The entry's id, the value the mark-read endpoint takes. | | `data.class` | string | yes | The alert class the person routes on Account Settings → Notifications: `approvals`, `assignments`, `mentions`, `planning`, `sprints`, `code`, `testing`, `releases`, `deployments`, `incidents`, `support`, `security`, `digest`, `onboarding` or `budget`. | | `data.class_label` | `Approvals`, `Assignments`, `Mentions`, `Planning`, `Sprints`, `Code`, `Testing`, `Releases`, `Deployments`, `Incidents`, `Support`, `Security`, `Digest`, `Onboarding`, `AI budget` | yes | The class's name in the person's language. | | `data.title` | string | yes | The alert's heading, the same words the email used, in the person's language at the time it was sent. | | `data.body` | string | yes | One sentence of context, the email's first line. | | `data.route` | string \| null | yes | The app screen the entry opens, as a named route, for a client that maps routes to screens; null when the alert points nowhere. | | `data.params` | object | yes | The named route's parameters; empty when it takes none. | | `data.url` | string \| null | yes | The same place resolved to a URL, for a client that does not map routes; null when the alert points nowhere. | | `data.read` | boolean | yes | Whether the person opened the entry. | | `data.read_at` | string \| null | yes | When it was opened, ISO 8601; null while unread. | | `data.created_at` | string \| null | yes | When the alert arrived, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: for an id that is not one of the caller's entries. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Mark every notification read Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-every-notification-read `POST /v1/me/notifications/read-all` ```bash curl -X POST https://api.example.test/v1/me/notifications/read-all \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Answers `200` with how many entries were unread and are read now. A second call marks nothing and answers `0`. - Requires ability: `account:write` - MCP tools: `mark_all_notifications_read` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: 200 with how many rows were marked. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.marked` | integer | yes | How many entries were unread and are read now. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Archive a persona Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/archive-a-persona `POST /v1/personas/{persona}/archive` ```bash curl -X POST https://api.example.test/v1/personas/$PERSONA_ID/archive \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the persona away: it leaves the Personas page's default view and the live listing, while the stories and use cases that name it keep resolving. Answers `200` with the persona, `archived_at` set, and emits `persona.archived`. - Requires ability: `persona:update` - Fires events: `persona.archived` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `persona` | path | string (uuid) | yes | The persona, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The persona, archived. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The persona's id, the value a story's `persona_id` will carry. | | `data.project_id` | string | yes | The project the persona belongs to. | | `data.name` | string | yes | The persona's name, unique within the project. | | `data.tagline` | string \| null | yes | One line that sums them up; null when none. | | `data.role_title` | string \| null | yes | What they do, in their own world; null when none. | | `data.demographics` | string \| null | yes | Who they are, as HTML filtered to a safe subset; null when none. | | `data.goals` | string \| null | yes | What they want from the product, as HTML; null when none. | | `data.frustrations` | string \| null | yes | What gets in their way today, as HTML; null when none. | | `data.behaviours` | string \| null | yes | How they work and what they reach for, as HTML; null when none. | | `data.tech_proficiency` | string | yes | How at ease they are with technology: `low`, `medium` or `high`. | | `data.devices` | array of any | yes | The devices they use, as short tags. | | `data.quote` | string \| null | yes | Something they would say; null when none. | | `data.position` | integer | yes | Their place among the project's personas, from zero. | | `data.archived_at` | string \| null | yes | When the persona was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the persona was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the persona last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `persona:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the persona is archived already. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Create a persona Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/create-a-persona `POST /v1/projects/{project}/personas` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/personas \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Busy Buyer", "tagline": "Orders for three branches between meetings.", "role_title": "Procurement officer", "tech_proficiency": "medium", "devices": ["Laptop", "Phone"] }' ``` Only `name` is required; it must be unique within the project, case-insensitively. `tech_proficiency` defaults to `medium`. The devices are kept as a list of up to ten short tags, duplicates folded regardless of case. Answers `201` with the persona and emits `persona.created`. - Requires ability: `persona:create` - Fires events: `persona.created` - MCP tools: `create_persona` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A persona to create or change. Only `name` is required on a create; the tech proficiency defaults to `medium`. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The persona's name, 2 to 80 characters, unique within the project regardless of case. | | `tagline` | string \| null | no | One line that sums them up, up to 160 characters. | | `role_title` | string \| null | no | What they do, in their own world, up to 80 characters. | | `demographics` | string \| null | no | Who they are, as HTML; filtered to a safe subset when read. | | `goals` | string \| null | no | What they want from the product, as HTML. | | `frustrations` | string \| null | no | What gets in their way today, as HTML. | | `behaviours` | string \| null | no | How they work and what they reach for, as HTML. | | `tech_proficiency` | `low`, `medium`, `high` | no | How at ease they are with technology: `low`, `medium` or `high`. Defaults to `medium` on a create. | | `devices` | array \| null | no | The devices they use, as up to ten short tags of at most 40 characters each. | | `quote` | string \| null | no | Something they would say, up to 300 characters. | ### Responses - **201**: The persona, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The persona's id, the value a story's `persona_id` will carry. | | `data.project_id` | string | yes | The project the persona belongs to. | | `data.name` | string | yes | The persona's name, unique within the project. | | `data.tagline` | string \| null | yes | One line that sums them up; null when none. | | `data.role_title` | string \| null | yes | What they do, in their own world; null when none. | | `data.demographics` | string \| null | yes | Who they are, as HTML filtered to a safe subset; null when none. | | `data.goals` | string \| null | yes | What they want from the product, as HTML; null when none. | | `data.frustrations` | string \| null | yes | What gets in their way today, as HTML; null when none. | | `data.behaviours` | string \| null | yes | How they work and what they reach for, as HTML; null when none. | | `data.tech_proficiency` | string | yes | How at ease they are with technology: `low`, `medium` or `high`. | | `data.devices` | array of any | yes | The devices they use, as short tags. | | `data.quote` | string \| null | yes | Something they would say; null when none. | | `data.position` | integer | yes | Their place among the project's personas, from zero. | | `data.archived_at` | string \| null | yes | When the persona was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the persona was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the persona last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `persona:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed or when another persona of the project already uses the name. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a persona Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/delete-a-persona `DELETE /v1/personas/{persona}` ```bash curl -X DELETE https://api.example.test/v1/personas/$PERSONA_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a persona with its activity history. Returns `204 No Content` and emits `persona.deleted`. Stories and use cases that will name personas are not here yet; once they are, a persona they name will be refused and archived instead. - Requires ability: `persona:delete` - Fires events: `persona.deleted` - MCP tools: `delete_persona` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `persona` | path | string (uuid) | yes | The persona, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `persona:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a persona Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/get-a-persona `GET /v1/personas/{persona}` ```bash curl https://api.example.test/v1/personas/$PERSONA_ID \ -H "Authorization: Bearer $TOKEN" ``` One persona. A persona of a project the token may not see is `404 RESOURCE_NOT_FOUND`. - `tech_proficiency` is `low`, `medium` or `high`; `devices` is a list of short tags. - `demographics`, `goals`, `frustrations` and `behaviours` are HTML filtered to a safe subset. - Requires ability: `persona:view` - MCP tools: `get_persona` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `persona` | path | string (uuid) | yes | The persona, bound by the middleware. | ### Responses - **200**: The persona. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The persona's id, the value a story's `persona_id` will carry. | | `data.project_id` | string | yes | The project the persona belongs to. | | `data.name` | string | yes | The persona's name, unique within the project. | | `data.tagline` | string \| null | yes | One line that sums them up; null when none. | | `data.role_title` | string \| null | yes | What they do, in their own world; null when none. | | `data.demographics` | string \| null | yes | Who they are, as HTML filtered to a safe subset; null when none. | | `data.goals` | string \| null | yes | What they want from the product, as HTML; null when none. | | `data.frustrations` | string \| null | yes | What gets in their way today, as HTML; null when none. | | `data.behaviours` | string \| null | yes | How they work and what they reach for, as HTML; null when none. | | `data.tech_proficiency` | string | yes | How at ease they are with technology: `low`, `medium` or `high`. | | `data.devices` | array of any | yes | The devices they use, as short tags. | | `data.quote` | string \| null | yes | Something they would say; null when none. | | `data.position` | integer | yes | Their place among the project's personas, from zero. | | `data.archived_at` | string \| null | yes | When the persona was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the persona was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the persona last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Personas API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas A **persona** is an archetypal user the project is built for: a named, sketched person with goals, frustrations and habits, so the analyst, the developers and the testers picture the same someone. Personas are the "as a …" of user stories and the actors of use cases; a persona that falls out of use is archived so those references keep resolving. The four prose fields and the devices are the analyst's sketch, and Heimdall reads them when it drafts stories and tests. The reads here see the personas of the projects the token may see, a client guest's token among them, because a persona describes the client's own users. A persona outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Personas page runs, under the project permissions, and announces itself as a `persona.*` event. ## Endpoints - `GET /v1/personas/{persona}` — [Get a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/get-a-persona) - `PATCH /v1/personas/{persona}` — [Update a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/update-a-persona) - `DELETE /v1/personas/{persona}` — [Delete a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/delete-a-persona) - `GET /v1/personas/{persona}/revisions` — [List a persona's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-a-personas-versions) - `GET /v1/personas/{persona}/revisions/{revision}` — [Read one version of a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/read-one-version-of-a-persona) - `GET /v1/projects/{project}/personas` — [List personas](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-personas) - `POST /v1/projects/{project}/personas` — [Create a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/create-a-persona) - `POST /v1/personas/{persona}/archive` — [Archive a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/archive-a-persona) - `POST /v1/personas/{persona}/restore` — [Restore a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/restore-a-persona) - `POST /v1/personas/{persona}/revisions/{revision}/restore` — [Restore a version of a persona](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/restore-a-version-of-a-persona) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project a persona belongs to. - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders): Who the project answers to, as opposed to who it is built for. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on a persona and who did it. - [persona.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona): The lifecycle of a project's personas: creation, edits, archiving, restoring and deletion. --- # List a persona's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-a-personas-versions `GET /v1/personas/{persona}/revisions` Every version the persona was saved as, newest first, each with who wrote it and why, the columns it changed and the diff from the version before. - Requires ability: `persona:view` - MCP tools: `get_persona` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `persona` | path | string (uuid) | yes | The persona, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List personas Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-personas `GET /v1/projects/{project}/personas` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/personas?archived=0" \ -H "Authorization: Bearer $TOKEN" ``` The personas of one project in the project's order, 25 per page (`per_page` 1 to 100). `sort_by` takes `name`, `tech_proficiency`, `position`, `created_at` or `updated_at`. `search` narrows the page; `archived=1` lists archived personas only and `archived=0` live ones only, with both listed when it is omitted. - Requires ability: `persona:view-any` - MCP tools: `list_personas` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `search` | query | string \| null | no | Words the name, the tagline or the role title must contain, up to 120 characters. | | `archived` | query | boolean \| null | no | `1` lists archived personas only, `0` live ones only; omit for both. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of personas. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The persona's id, the value a story's `persona_id` will carry. | | `data[].project_id` | string | yes | The project the persona belongs to. | | `data[].name` | string | yes | The persona's name, unique within the project. | | `data[].tagline` | string \| null | yes | One line that sums them up; null when none. | | `data[].role_title` | string \| null | yes | What they do, in their own world; null when none. | | `data[].demographics` | string \| null | yes | Who they are, as HTML filtered to a safe subset; null when none. | | `data[].goals` | string \| null | yes | What they want from the product, as HTML; null when none. | | `data[].frustrations` | string \| null | yes | What gets in their way today, as HTML; null when none. | | `data[].behaviours` | string \| null | yes | How they work and what they reach for, as HTML; null when none. | | `data[].tech_proficiency` | string | yes | How at ease they are with technology: `low`, `medium` or `high`. | | `data[].devices` | array of any | yes | The devices they use, as short tags. | | `data[].quote` | string \| null | yes | Something they would say; null when none. | | `data[].position` | integer | yes | Their place among the project's personas, from zero. | | `data[].archived_at` | string \| null | yes | When the persona was archived, ISO 8601; null while live. | | `data[].created_at` | string \| null | yes | When the persona was created, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the persona last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `search` is longer than 120 characters or `archived` is not a boolean. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Read one version of a persona Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/read-one-version-of-a-persona `GET /v1/personas/{persona}/revisions/{revision}` One version with its full snapshot, the diff from the version before, who wrote it and, for a restore, the version it put back. - Requires ability: `persona:view` - MCP tools: `get_persona` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `persona` | path | string (uuid) | yes | The persona, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version with its snapshot and diff. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a persona Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/restore-a-persona `POST /v1/personas/{persona}/restore` ```bash curl -X POST https://api.example.test/v1/personas/$PERSONA_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Brings an archived persona back. Answers `200` with the persona, `archived_at` null again, and emits `persona.restored`. - Requires ability: `persona:update` - Fires events: `persona.restored` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `persona` | path | string (uuid) | yes | The persona, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The persona, live again. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The persona's id, the value a story's `persona_id` will carry. | | `data.project_id` | string | yes | The project the persona belongs to. | | `data.name` | string | yes | The persona's name, unique within the project. | | `data.tagline` | string \| null | yes | One line that sums them up; null when none. | | `data.role_title` | string \| null | yes | What they do, in their own world; null when none. | | `data.demographics` | string \| null | yes | Who they are, as HTML filtered to a safe subset; null when none. | | `data.goals` | string \| null | yes | What they want from the product, as HTML; null when none. | | `data.frustrations` | string \| null | yes | What gets in their way today, as HTML; null when none. | | `data.behaviours` | string \| null | yes | How they work and what they reach for, as HTML; null when none. | | `data.tech_proficiency` | string | yes | How at ease they are with technology: `low`, `medium` or `high`. | | `data.devices` | array of any | yes | The devices they use, as short tags. | | `data.quote` | string \| null | yes | Something they would say; null when none. | | `data.position` | integer | yes | Their place among the project's personas, from zero. | | `data.archived_at` | string \| null | yes | When the persona was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the persona was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the persona last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `persona:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the persona is not archived. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a persona Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/restore-a-version-of-a-persona `POST /v1/personas/{persona}/revisions/{revision}/restore` Fills the version's snapshot back onto the persona and writes a new version that points at it; nothing earlier is rewritten. - Requires ability: `persona:update` - Fires events: `persona.version_restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `persona` | path | string (uuid) | yes | The persona, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version written, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a persona Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/update-a-persona `PATCH /v1/personas/{persona}` ```bash curl -X PATCH https://api.example.test/v1/personas/$PERSONA_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"quote": "Just let me repeat last month'"'"'s order.", "devices": ["Laptop", "Phone", "Tablet"]}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. A new name must be free within the project. Answers `200` with the persona and emits `persona.updated` with the columns that changed, each with its old and new value, the devices as a list; an update that changes nothing answers `200` and emits nothing. - Requires ability: `persona:update` - Fires events: `persona.updated` - MCP tools: `update_persona` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `persona` | path | string (uuid) | yes | The persona, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A persona to create or change. Only `name` is required on a create; the tech proficiency defaults to `medium`. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The persona's name, 2 to 80 characters, unique within the project regardless of case. | | `tagline` | string \| null | no | One line that sums them up, up to 160 characters. | | `role_title` | string \| null | no | What they do, in their own world, up to 80 characters. | | `demographics` | string \| null | no | Who they are, as HTML; filtered to a safe subset when read. | | `goals` | string \| null | no | What they want from the product, as HTML. | | `frustrations` | string \| null | no | What gets in their way today, as HTML. | | `behaviours` | string \| null | no | How they work and what they reach for, as HTML. | | `tech_proficiency` | `low`, `medium`, `high` | no | How at ease they are with technology: `low`, `medium` or `high`. Defaults to `medium` on a create. | | `devices` | array \| null | no | The devices they use, as up to ten short tags of at most 40 characters each. | | `quote` | string \| null | no | Something they would say, up to 300 characters. | ### Responses - **200**: The persona. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The persona's id, the value a story's `persona_id` will carry. | | `data.project_id` | string | yes | The project the persona belongs to. | | `data.name` | string | yes | The persona's name, unique within the project. | | `data.tagline` | string \| null | yes | One line that sums them up; null when none. | | `data.role_title` | string \| null | yes | What they do, in their own world; null when none. | | `data.demographics` | string \| null | yes | Who they are, as HTML filtered to a safe subset; null when none. | | `data.goals` | string \| null | yes | What they want from the product, as HTML; null when none. | | `data.frustrations` | string \| null | yes | What gets in their way today, as HTML; null when none. | | `data.behaviours` | string \| null | yes | How they work and what they reach for, as HTML; null when none. | | `data.tech_proficiency` | string | yes | How at ease they are with technology: `low`, `medium` or `high`. | | `data.devices` | array of any | yes | The devices they use, as short tags. | | `data.quote` | string \| null | yes | Something they would say; null when none. | | `data.position` | integer | yes | Their place among the project's personas, from zero. | | `data.archived_at` | string \| null | yes | When the persona was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the persona was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the persona last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `persona:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed or when the new name is already used by another persona of the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Check the API is up Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/ping/check-the-api-is-up `GET /v1/ping` ```bash curl https://api.example.test/v1/ping ``` Answers `200` with `data.status` of `ok`, the API version and the current server time in ISO 8601. No `Authorization` header is read and no rate limit applies, so a monitor may call it on a schedule. ### Responses - **200**: The status, the API version and the current time. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.status` | string | yes | Always `ok`: the application answered. | | `data.version` | string | yes | The API version the host serves, `v1`. | | `data.timestamp` | string | yes | The server's current time, ISO 8601 with offset. | --- # Ping API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/ping The one endpoint that needs no token. It answers from the API host itself, so a `200` proves the host resolves, TLS terminates and the application is up; it says nothing about your token, which `GET /v1/teams/current` is the probe for. ## Endpoints - `GET /v1/ping` — [Check the API is up](https://alm-docs.labs.zuselab.dev/api/v1/reference/ping/check-the-api-is-up) --- # Get a project's settings Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/get-a-projects-settings `GET /v1/projects/{project}/settings` ```bash curl https://api.example.test/v1/projects/$PROJECT_ID/settings \ -H "Authorization: Bearer $TOKEN" ``` Every setting of one project, the defaults filled in where the project never changed a key. `working_days` lists the days of the week from 1 for Monday to 7 for Sunday; `business_hours` carries `start` and `end` as `HH:MM` in the project's timezone; `wip_limits` is a map from status code to the most items a column holds and `hidden_columns` a list of status codes, both empty until the board sets them. - Requires ability: `project:view` - MCP tools: `get_project_settings` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | ### Responses - **200**: Every setting, the defaults filled in. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.project_id` | string | yes | The project the settings belong to. | | `data.sprint_length_days` | integer | yes | How long a sprint runs, in days, 1 to 60. | | `data.working_days` | array of integer | yes | The days of the week work happens, 1 for Monday to 7 for Sunday, ascending. | | `data.timezone` | string \| null | yes | The zone sprint boundaries and SLAs are read in; null when the workspace default applies. | | `data.estimation_unit` | string | yes | What work is sized in: `points` or `hours`. | | `data.story_point_scale` | string | yes | The steps story points may take: `fibonacci`, `linear` or `tshirt`. | | `data.default_assignee_rule` | string | yes | Who a new item goes to when nobody is named: `reporter`, `lead` or `none`. | | `data.auto_transition_on_pr` | boolean | yes | Whether an opened pull request moves its item to review. | | `data.auto_transition_on_merge` | boolean | yes | Whether a merged pull request moves its item on. | | `data.require_dor_for_sprint` | boolean | yes | Whether a story needs its Definition of Ready ticked before a sprint takes it. | | `data.require_dod_for_done` | boolean | yes | Whether an item needs its Definition of Done ticked before it is done. | | `data.guest_can_comment` | boolean | yes | Whether a client guest may comment. | | `data.ai_assist_enabled` | boolean | yes | Whether Heimdall's assists are offered on this project. | | `data.telegram_group_chat_id` | string \| null | yes | The Telegram group chat that receives the project's alerts; null when none. | | `data.wip_limits` | object | yes | The most items a board column holds, keyed by status code; empty when no column is limited. | | `data.hidden_columns` | array of string | yes | The board columns the project does not show, by status code. | | `data.business_hours` | object | yes | The hours SLAs count, as `HH:MM` in the project's timezone. | | `data.business_hours.start` | string | yes | When the business day starts. | | `data.business_hours.end` | string | yes | When the business day ends, after the start. | | `data.auto_incident_on_down` | boolean | yes | Whether a production environment found down twice in a row opens an incident. | | `data.council_voices` | array of any | yes | The voices of Heimdall's council that sit per moment, keyed by moment, each a list of voice keys; every open moment is present. | | `data.council_budget_percent` | integer | yes | The share of the team's monthly AI budget the council may spend on this project, as a percentage. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token belongs to a client guest, who may read the project but not its settings. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Project Settings API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings A project's **settings** are its per-project knobs: how long a sprint runs and on which days, which timezone its deadlines are read in, what work is sized in and on which scale, who a new item goes to, whether pull requests move items along and whether checklists gate a sprint or a done, whether a client guest may comment, whether Heimdall assists, where alerts go on Telegram, the board's limits and hidden columns, the business hours SLAs count and whether a production outage opens an incident. A project stores only the keys somebody changed; the read answers every key with the workspace default filled in. The read is for staff: a project's settings are the team's business, so a client guest's token is refused with a `403 FORBIDDEN` even though the guest may read the project. The write runs the same action the project's Settings page runs, under `project:update`, and announces the keys that changed as `project.settings_changed`. ## Endpoints - `GET /v1/projects/{project}/settings` — [Get a project's settings](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/get-a-projects-settings) - `PATCH /v1/projects/{project}/settings` — [Update a project's settings](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/update-a-projects-settings) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project the settings belong to. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on the project and who did it. - [project.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project): The lifecycle of a project and its membership: creation, edits, status moves, archiving, restoring, deletion, and people joining or leaving it. --- # Update a project's settings Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/update-a-projects-settings `PATCH /v1/projects/{project}/settings` ```bash curl -X PATCH https://api.example.test/v1/projects/$PROJECT_ID/settings \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"sprint_length_days": 10, "working_days": [1, 2, 3, 4], "estimation_unit": "hours"}' ``` Send only the keys to change: a key left out keeps its value, and `timezone` or `telegram_group_chat_id` sent as `null` means none. A sprint runs 1 to 60 days; the working days are at least one and none twice; the business day ends after it starts; the Telegram chat id is digits with an optional leading minus. Answers `200` with every setting as it is now and emits `project.settings_changed` with the keys that changed, each with its old and new value; a request that changes nothing answers `200` and emits nothing. - Requires ability: `project:update` - Fires events: `project.settings_changed` - MCP tools: `update_project_settings` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The settings to change. Every key is optional; a key left out keeps its value and `timezone` or `telegram_group_chat_id` sent as null means none. | Field | Type | Required | Description | | --- | --- | --- | --- | | `sprint_length_days` | integer | no | How long a sprint runs, in days, 1 to 60. | | `working_days` | array of integer | no | The days of the week work happens, 1 for Monday to 7 for Sunday, at least one and none twice. | | `timezone` | string \| null | no | A timezone identifier such as `Asia/Colombo`, or null for the workspace default. | | `estimation_unit` | `points`, `hours` | no | `points` or `hours`. | | `story_point_scale` | `fibonacci`, `linear`, `tshirt` | no | `fibonacci`, `linear` or `tshirt`. | | `default_assignee_rule` | `reporter`, `lead`, `none` | no | `reporter`, `lead` or `none`. | | `auto_transition_on_pr` | boolean | no | Whether an opened pull request moves its item to review. | | `auto_transition_on_merge` | boolean | no | Whether a merged pull request moves its item on. | | `require_dor_for_sprint` | boolean | no | Whether a story needs its Definition of Ready ticked before a sprint takes it. | | `require_dod_for_done` | boolean | no | Whether an item needs its Definition of Done ticked before it is done. | | `guest_can_comment` | boolean | no | Whether a client guest may comment. | | `ai_assist_enabled` | boolean | no | Whether Heimdall's assists are offered on this project. | | `telegram_group_chat_id` | string \| null | no | The Telegram group chat id, digits with an optional leading minus, or null for none. | | `wip_limits` | array of integer | no | The most items a board column holds, keyed by status code, each 0 to 100; up to twenty columns. | | `hidden_columns` | array of string | no | The board columns to hide, by status code, up to twenty. | | `business_hours` | object | no | The hours SLAs count: `start` and `end` as `HH:MM`, the end after the start. | | `business_hours.start` | string | no | When the business day starts, `HH:MM`. | | `business_hours.end` | string | no | When the business day ends, `HH:MM`, after the start. | | `auto_incident_on_down` | boolean | no | Whether a production environment found down twice in a row opens an incident. | | `council_voices` | array of array of `odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur`, `hel` | no | The voices of Heimdall's council that sit per moment, keyed by moment (`intake_scaffold`, `requirement_review`), each a list of voice keys; a moment left out keeps its default seats, a moment sent empty seats nobody. | | `council_budget_percent` | integer | no | The share of the team's monthly AI budget the council may spend on this project, 1 to 100. | ### Responses - **200**: Every setting as it is now. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.project_id` | string | yes | The project the settings belong to. | | `data.sprint_length_days` | integer | yes | How long a sprint runs, in days, 1 to 60. | | `data.working_days` | array of integer | yes | The days of the week work happens, 1 for Monday to 7 for Sunday, ascending. | | `data.timezone` | string \| null | yes | The zone sprint boundaries and SLAs are read in; null when the workspace default applies. | | `data.estimation_unit` | string | yes | What work is sized in: `points` or `hours`. | | `data.story_point_scale` | string | yes | The steps story points may take: `fibonacci`, `linear` or `tshirt`. | | `data.default_assignee_rule` | string | yes | Who a new item goes to when nobody is named: `reporter`, `lead` or `none`. | | `data.auto_transition_on_pr` | boolean | yes | Whether an opened pull request moves its item to review. | | `data.auto_transition_on_merge` | boolean | yes | Whether a merged pull request moves its item on. | | `data.require_dor_for_sprint` | boolean | yes | Whether a story needs its Definition of Ready ticked before a sprint takes it. | | `data.require_dod_for_done` | boolean | yes | Whether an item needs its Definition of Done ticked before it is done. | | `data.guest_can_comment` | boolean | yes | Whether a client guest may comment. | | `data.ai_assist_enabled` | boolean | yes | Whether Heimdall's assists are offered on this project. | | `data.telegram_group_chat_id` | string \| null | yes | The Telegram group chat that receives the project's alerts; null when none. | | `data.wip_limits` | object | yes | The most items a board column holds, keyed by status code; empty when no column is limited. | | `data.hidden_columns` | array of string | yes | The board columns the project does not show, by status code. | | `data.business_hours` | object | yes | The hours SLAs count, as `HH:MM` in the project's timezone. | | `data.business_hours.start` | string | yes | When the business day starts. | | `data.business_hours.end` | string | yes | When the business day ends, after the start. | | `data.auto_incident_on_down` | boolean | yes | Whether a production environment found down twice in a row opens an incident. | | `data.council_voices` | array of any | yes | The voices of Heimdall's council that sit per moment, keyed by moment, each a list of voice keys; every open moment is present. | | `data.council_budget_percent` | integer | yes | The share of the team's monthly AI budget the council may spend on this project, as a percentage. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `project:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a key is malformed: a sprint outside 1 to 60 days, a day outside 1 to 7 or listed twice, an unknown timezone, unit, scale or rule, a business day that ends before it starts, or a Telegram chat id that is not a number. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Archive a project Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/archive-a-project `POST /v1/projects/{project}/archive` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/archive \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the project away: its status reads `archived`, it leaves the registry's default view, and everything under it is kept. The status it left is remembered for the restore. Answers `200` with the project and emits `project.status_changed` followed by `project.archived`. - Requires ability: `project:update` - Fires events: `project.archived` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The project with its archive stamp. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The project's id, the value every project-scoped endpoint takes in its path. | | `data.team_id` | string | yes | The team the project belongs to. | | `data.client_id` | string \| null | yes | The client the project is for; null for an internal project. | | `data.key` | string | yes | The project's key, two to six upper-case letters and digits, the prefix of every reference such as `CRM-142`. Immutable once an artefact exists. | | `data.name` | string | yes | The project's name, unique within the team. | | `data.tagline` | string \| null | yes | One line about the project; null when none. | | `data.description` | string \| array of any \| null | yes | What the project is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the project is in its lifecycle: `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` or `archived`. | | `data.methodology` | string | yes | How the project is run: `scrum`, `kanban`, `hybrid` or `waterfall`. | | `data.visibility` | string | yes | `team` when every member of the team sees it, `members_only` when only its project members do. | | `data.health` | string | yes | The project's health as last computed: `on_track`, `at_risk`, `off_track` or `unknown`. | | `data.lead_pm_id` | string \| null | yes | The project manager; null when unassigned. | | `data.lead_ba_id` | string \| null | yes | The lead business analyst; null when unassigned. | | `data.lead_dev_id` | string \| null | yes | The lead developer; null when unassigned. | | `data.lead_qa_id` | string \| null | yes | The QA lead; null when unassigned. | | `data.starts_on` | string \| null | yes | The planned start, `YYYY-MM-DD`; null when not set. | | `data.target_launch_on` | string \| null | yes | The target launch date, `YYYY-MM-DD`; null when not set. | | `data.launched_on` | string \| null | yes | The day the project went live, `YYYY-MM-DD`; null until it did. | | `data.budget_hours` | integer \| null | yes | The budgeted effort in hours; null when none was agreed. | | `data.archived_at` | string \| null | yes | When the project was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the project was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the project last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder may not change the project. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the project is archived already. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Create a project Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/create-a-project `POST /v1/projects` ```bash curl -X POST https://api.example.test/v1/projects \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Customer Portal", "key": "CRM", "client_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "tagline": "Self-service for retail customers", "methodology": "scrum", "lead_pm_id": "0c2d8a7e-4b1f-4d3e-9a6b-2f5e8c1d7a90", "target_launch_on": "2027-03-31", "budget_hours": 1200 }' ``` Only `name` and `key` are required. The key is upper-cased, is two to six letters and digits starting with a letter, and must be free across everything the team ever had, archived and deleted projects included, because the references it prefixed stay in people's hands. The name is unique within the team. `client_id` must be one of the team's clients, and each lead must be a member of the team. `status` defaults to `discovery`, `methodology` to `scrum` and `visibility` to `team`; `archived` is never a status to send, archiving has its own endpoint. > **A paragraph is enough.** A project created with a name, a key and a description is ready for Heimdall to draft its components, environments, personas and epics from; the rest of the record can follow as the intake proceeds. Answers `201` with the project and emits `project.created`. - Requires ability: `project:create` - Fires events: `project.created` - MCP tools: `create_project` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A project to create or change. Only `name` and `key` are required on a create; on an update, a field left out keeps its value and a field sent as null is cleared. The key is upper-cased. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The project's name, 3 to 100 characters, unique within the team. | | `key` | string | yes | The key, two to six upper-case letters and digits starting with a letter, unique across everything the team ever had. Frozen once an artefact carries it. | | `client_id` | string \| null (uuid) | no | The client the project is for, one of the team's; null for an internal project. | | `tagline` | string \| null | no | One line about the project, up to 160 characters. | | `description` | string \| null | no | What the project is, as HTML; filtered to a safe subset when read. | | `status` | `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold`, `archived` | no | The lifecycle status: `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance` or `on_hold`. Defaults to `discovery` on a create; `archived` is set through the archive endpoint, never here. | | `methodology` | `scrum`, `kanban`, `hybrid`, `waterfall` | no | How the project is run: `scrum`, `kanban`, `hybrid` or `waterfall`. Defaults to `scrum`. | | `visibility` | `team`, `members_only` | no | `team` when every member of the team sees it, `members_only` when only its project members do. Defaults to `team`. | | `lead_pm_id` | string \| null (uuid) | no | The project manager, a member of the team; null to leave the seat empty. | | `lead_ba_id` | string \| null (uuid) | no | The lead business analyst, a member of the team; null to leave the seat empty. | | `lead_dev_id` | string \| null (uuid) | no | The lead developer, a member of the team; null to leave the seat empty. | | `lead_qa_id` | string \| null (uuid) | no | The QA lead, a member of the team; null to leave the seat empty. | | `starts_on` | string \| null (date) | no | The planned start, `YYYY-MM-DD`. | | `target_launch_on` | string \| null (date) | no | The target launch, `YYYY-MM-DD`, on or after the start. | | `launched_on` | string \| null (date) | no | The day the project went live, `YYYY-MM-DD`. | | `budget_hours` | integer \| null | no | The budgeted effort in whole hours, up to a million. | ### Responses - **201**: The project as a 201. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The project's id, the value every project-scoped endpoint takes in its path. | | `data.team_id` | string | yes | The team the project belongs to. | | `data.client_id` | string \| null | yes | The client the project is for; null for an internal project. | | `data.key` | string | yes | The project's key, two to six upper-case letters and digits, the prefix of every reference such as `CRM-142`. Immutable once an artefact exists. | | `data.name` | string | yes | The project's name, unique within the team. | | `data.tagline` | string \| null | yes | One line about the project; null when none. | | `data.description` | string \| array of any \| null | yes | What the project is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the project is in its lifecycle: `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` or `archived`. | | `data.methodology` | string | yes | How the project is run: `scrum`, `kanban`, `hybrid` or `waterfall`. | | `data.visibility` | string | yes | `team` when every member of the team sees it, `members_only` when only its project members do. | | `data.health` | string | yes | The project's health as last computed: `on_track`, `at_risk`, `off_track` or `unknown`. | | `data.lead_pm_id` | string \| null | yes | The project manager; null when unassigned. | | `data.lead_ba_id` | string \| null | yes | The lead business analyst; null when unassigned. | | `data.lead_dev_id` | string \| null | yes | The lead developer; null when unassigned. | | `data.lead_qa_id` | string \| null | yes | The QA lead; null when unassigned. | | `data.starts_on` | string \| null | yes | The planned start, `YYYY-MM-DD`; null when not set. | | `data.target_launch_on` | string \| null | yes | The target launch date, `YYYY-MM-DD`; null when not set. | | `data.launched_on` | string \| null | yes | The day the project went live, `YYYY-MM-DD`; null until it did. | | `data.budget_hours` | integer \| null | yes | The budgeted effort in hours; null when none was agreed. | | `data.archived_at` | string \| null | yes | When the project was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the project was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the project last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role does not carry `project:create`. - **404**: `RESOURCE_NOT_FOUND`: when `client_id` is not one of the team's clients. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, the key or the name is taken (`error.context.key` or `error.context.name` names it), or a lead is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a project Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/delete-a-project `DELETE /v1/projects/{project}` ```bash curl -X DELETE https://api.example.test/v1/projects/$PROJECT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes the project with everything under it; the key becomes free to reuse. On the registry page the person types the key to confirm, over the API the request is the confirmation. Returns `204 No Content` and emits `project.deleted`. - Requires ability: `project:delete` - Fires events: `project.deleted` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder may not delete the project. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a project Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/get-a-project `GET /v1/projects/{project}` ```bash curl https://api.example.test/v1/projects/$PROJECT_ID \ -H "Authorization: Bearer $TOKEN" ``` One project. A project outside the token's team or its project scope is `404 RESOURCE_NOT_FOUND`. - `status` runs `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` and `archived`; `health` is `on_track`, `at_risk`, `off_track` or `unknown`, recomputed nightly from overdue items, failing runs, open incidents and stuck approvals once those modules land. - `visibility` is `team` when every member of the team sees the project and `members_only` when only its project members do; a client guest always sees a project through a membership row, whatever its visibility. - `description` is HTML filtered to a safe subset; `starts_on`, `target_launch_on` and `launched_on` are dates, not timestamps. - Requires ability: `project:view` - MCP tools: `get_project` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, resolved by the route binder. | ### Responses - **200**: The project. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The project's id, the value every project-scoped endpoint takes in its path. | | `data.team_id` | string | yes | The team the project belongs to. | | `data.client_id` | string \| null | yes | The client the project is for; null for an internal project. | | `data.key` | string | yes | The project's key, two to six upper-case letters and digits, the prefix of every reference such as `CRM-142`. Immutable once an artefact exists. | | `data.name` | string | yes | The project's name, unique within the team. | | `data.tagline` | string \| null | yes | One line about the project; null when none. | | `data.description` | string \| array of any \| null | yes | What the project is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the project is in its lifecycle: `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` or `archived`. | | `data.methodology` | string | yes | How the project is run: `scrum`, `kanban`, `hybrid` or `waterfall`. | | `data.visibility` | string | yes | `team` when every member of the team sees it, `members_only` when only its project members do. | | `data.health` | string | yes | The project's health as last computed: `on_track`, `at_risk`, `off_track` or `unknown`. | | `data.lead_pm_id` | string \| null | yes | The project manager; null when unassigned. | | `data.lead_ba_id` | string \| null | yes | The lead business analyst; null when unassigned. | | `data.lead_dev_id` | string \| null | yes | The lead developer; null when unassigned. | | `data.lead_qa_id` | string \| null | yes | The QA lead; null when unassigned. | | `data.starts_on` | string \| null | yes | The planned start, `YYYY-MM-DD`; null when not set. | | `data.target_launch_on` | string \| null | yes | The target launch date, `YYYY-MM-DD`; null when not set. | | `data.launched_on` | string \| null | yes | The day the project went live, `YYYY-MM-DD`; null until it did. | | `data.budget_hours` | integer \| null | yes | The budgeted effort in hours; null when none was agreed. | | `data.archived_at` | string \| null | yes | When the project was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the project was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the project last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Projects API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/projects A **project** is one client engagement from discovery to maintenance: the container every component, environment, epic, story, requirement, test, release and incident lives under. It carries a short **key** (`CRM`) that prefixes every reference in it (`CRM-142`), a lifecycle **status**, the **methodology** it is run with, its **visibility** within the team, its four leads and its dates. The reads here see the registry as the token sees it: every project of the team with `visibility: team`, plus the ones the person holds a membership row on, narrowed further by the token's `scope:project:` entries. A project outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`, so the API cannot be used to learn that a project exists. Every write runs the same action the registry page runs, under the same permissions (the team owner, then the person's project role, then their team role), and announces itself as a `project.*` event. ## Endpoints - `GET /v1/projects` — [List projects](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/list-projects) - `POST /v1/projects` — [Create a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/create-a-project) - `GET /v1/projects/{project}` — [Get a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/get-a-project) - `PATCH /v1/projects/{project}` — [Update a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/update-a-project) - `DELETE /v1/projects/{project}` — [Delete a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/delete-a-project) - `POST /v1/projects/{project}/archive` — [Archive a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/archive-a-project) - `POST /v1/projects/{project}/restore` — [Restore a project](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/restore-a-project) ## Related - [Clients API](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients): The organisation a project is done for. - [Teams API](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams): The tenant a project belongs to. - [Team Members API](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members): Inviting a client guest onto one project. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on a project and who did it. - [project.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project): The lifecycle of a project and its membership: creation, edits, status moves, archiving, restoring, deletion, and people joining or leaving it. --- # List projects Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/list-projects `GET /v1/projects` ```bash curl "https://api.example.test/v1/projects?status=in_development" \ -H "Authorization: Bearer $TOKEN" ``` Every project visible to the token, newest first, 25 per page (`per_page` 1 to 100). `sort_by` takes `name`, `key`, `status`, `health`, `created_at`, `updated_at` or `target_launch_on`. Archived projects are left out unless `archived=1` is sent; `status`, `client_id` and `search` narrow the page. A token minted with `scope:project:` entries lists only those projects. - Requires ability: `project:view-any` - MCP tools: `list_projects` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold`, `archived` \| null | no | Narrow to one lifecycle status, such as `in_development`. An unknown status is refused. | | `client_id` | query | string \| null (uuid) | no | Narrow to the projects of one client. | | `search` | query | string \| null | no | Words the key or name must contain, up to 120 characters. | | `archived` | query | boolean \| null | no | `1` includes archived projects, which are left out by default. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: The page, in name order. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The project's id, the value every project-scoped endpoint takes in its path. | | `data[].team_id` | string | yes | The team the project belongs to. | | `data[].client_id` | string \| null | yes | The client the project is for; null for an internal project. | | `data[].key` | string | yes | The project's key, two to six upper-case letters and digits, the prefix of every reference such as `CRM-142`. Immutable once an artefact exists. | | `data[].name` | string | yes | The project's name, unique within the team. | | `data[].tagline` | string \| null | yes | One line about the project; null when none. | | `data[].description` | string \| array of any \| null | yes | What the project is, as HTML filtered to a safe subset; null when none. | | `data[].status` | string | yes | Where the project is in its lifecycle: `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` or `archived`. | | `data[].methodology` | string | yes | How the project is run: `scrum`, `kanban`, `hybrid` or `waterfall`. | | `data[].visibility` | string | yes | `team` when every member of the team sees it, `members_only` when only its project members do. | | `data[].health` | string | yes | The project's health as last computed: `on_track`, `at_risk`, `off_track` or `unknown`. | | `data[].lead_pm_id` | string \| null | yes | The project manager; null when unassigned. | | `data[].lead_ba_id` | string \| null | yes | The lead business analyst; null when unassigned. | | `data[].lead_dev_id` | string \| null | yes | The lead developer; null when unassigned. | | `data[].lead_qa_id` | string \| null | yes | The QA lead; null when unassigned. | | `data[].starts_on` | string \| null | yes | The planned start, `YYYY-MM-DD`; null when not set. | | `data[].target_launch_on` | string \| null | yes | The target launch date, `YYYY-MM-DD`; null when not set. | | `data[].launched_on` | string \| null | yes | The day the project went live, `YYYY-MM-DD`; null until it did. | | `data[].budget_hours` | integer \| null | yes | The budgeted effort in hours; null when none was agreed. | | `data[].archived_at` | string \| null | yes | When the project was archived, ISO 8601; null while live. | | `data[].created_at` | string \| null | yes | When the project was created, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the project last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is not a lifecycle status, `client_id` is not a UUID, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a project Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/restore-a-project `POST /v1/projects/{project}/restore` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Brings an archived project back with the status it had before it was archived. Answers `200` with the project and emits `project.status_changed` followed by `project.restored`. - Requires ability: `project:update` - Fires events: `project.restored` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The project, live again with the status it had. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The project's id, the value every project-scoped endpoint takes in its path. | | `data.team_id` | string | yes | The team the project belongs to. | | `data.client_id` | string \| null | yes | The client the project is for; null for an internal project. | | `data.key` | string | yes | The project's key, two to six upper-case letters and digits, the prefix of every reference such as `CRM-142`. Immutable once an artefact exists. | | `data.name` | string | yes | The project's name, unique within the team. | | `data.tagline` | string \| null | yes | One line about the project; null when none. | | `data.description` | string \| array of any \| null | yes | What the project is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the project is in its lifecycle: `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` or `archived`. | | `data.methodology` | string | yes | How the project is run: `scrum`, `kanban`, `hybrid` or `waterfall`. | | `data.visibility` | string | yes | `team` when every member of the team sees it, `members_only` when only its project members do. | | `data.health` | string | yes | The project's health as last computed: `on_track`, `at_risk`, `off_track` or `unknown`. | | `data.lead_pm_id` | string \| null | yes | The project manager; null when unassigned. | | `data.lead_ba_id` | string \| null | yes | The lead business analyst; null when unassigned. | | `data.lead_dev_id` | string \| null | yes | The lead developer; null when unassigned. | | `data.lead_qa_id` | string \| null | yes | The QA lead; null when unassigned. | | `data.starts_on` | string \| null | yes | The planned start, `YYYY-MM-DD`; null when not set. | | `data.target_launch_on` | string \| null | yes | The target launch date, `YYYY-MM-DD`; null when not set. | | `data.launched_on` | string \| null | yes | The day the project went live, `YYYY-MM-DD`; null until it did. | | `data.budget_hours` | integer \| null | yes | The budgeted effort in hours; null when none was agreed. | | `data.archived_at` | string \| null | yes | When the project was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the project was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the project last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder may not change the project. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the project is not archived. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a project Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/update-a-project `PATCH /v1/projects/{project}` ```bash curl -X PATCH https://api.example.test/v1/projects/$PROJECT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "in_development", "lead_dev_id": "5e0c7a2d-9b1f-4c3e-8d6a-2f4b7c9e1a30"}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. The key may change until an artefact is numbered under the project; from then on it is frozen, because references such as `CRM-142` already carry it. A status move inside the edit is announced as `project.status_changed`; the other columns that changed travel as `project.updated` with their old and new values. An update that changes nothing answers `200` and emits nothing. An archived project cannot be edited this way: restore it first. Answers `200` with the project. - Requires ability: `project:update` - Fires events: `project.updated` - MCP tools: `update_project` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A project to create or change. Only `name` and `key` are required on a create; on an update, a field left out keeps its value and a field sent as null is cleared. The key is upper-cased. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The project's name, 3 to 100 characters, unique within the team. | | `key` | string | yes | The key, two to six upper-case letters and digits starting with a letter, unique across everything the team ever had. Frozen once an artefact carries it. | | `client_id` | string \| null (uuid) | no | The client the project is for, one of the team's; null for an internal project. | | `tagline` | string \| null | no | One line about the project, up to 160 characters. | | `description` | string \| null | no | What the project is, as HTML; filtered to a safe subset when read. | | `status` | `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold`, `archived` | no | The lifecycle status: `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance` or `on_hold`. Defaults to `discovery` on a create; `archived` is set through the archive endpoint, never here. | | `methodology` | `scrum`, `kanban`, `hybrid`, `waterfall` | no | How the project is run: `scrum`, `kanban`, `hybrid` or `waterfall`. Defaults to `scrum`. | | `visibility` | `team`, `members_only` | no | `team` when every member of the team sees it, `members_only` when only its project members do. Defaults to `team`. | | `lead_pm_id` | string \| null (uuid) | no | The project manager, a member of the team; null to leave the seat empty. | | `lead_ba_id` | string \| null (uuid) | no | The lead business analyst, a member of the team; null to leave the seat empty. | | `lead_dev_id` | string \| null (uuid) | no | The lead developer, a member of the team; null to leave the seat empty. | | `lead_qa_id` | string \| null (uuid) | no | The QA lead, a member of the team; null to leave the seat empty. | | `starts_on` | string \| null (date) | no | The planned start, `YYYY-MM-DD`. | | `target_launch_on` | string \| null (date) | no | The target launch, `YYYY-MM-DD`, on or after the start. | | `launched_on` | string \| null (date) | no | The day the project went live, `YYYY-MM-DD`. | | `budget_hours` | integer \| null | no | The budgeted effort in whole hours, up to a million. | ### Responses - **200**: The updated project. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The project's id, the value every project-scoped endpoint takes in its path. | | `data.team_id` | string | yes | The team the project belongs to. | | `data.client_id` | string \| null | yes | The client the project is for; null for an internal project. | | `data.key` | string | yes | The project's key, two to six upper-case letters and digits, the prefix of every reference such as `CRM-142`. Immutable once an artefact exists. | | `data.name` | string | yes | The project's name, unique within the team. | | `data.tagline` | string \| null | yes | One line about the project; null when none. | | `data.description` | string \| array of any \| null | yes | What the project is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where the project is in its lifecycle: `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` or `archived`. | | `data.methodology` | string | yes | How the project is run: `scrum`, `kanban`, `hybrid` or `waterfall`. | | `data.visibility` | string | yes | `team` when every member of the team sees it, `members_only` when only its project members do. | | `data.health` | string | yes | The project's health as last computed: `on_track`, `at_risk`, `off_track` or `unknown`. | | `data.lead_pm_id` | string \| null | yes | The project manager; null when unassigned. | | `data.lead_ba_id` | string \| null | yes | The lead business analyst; null when unassigned. | | `data.lead_dev_id` | string \| null | yes | The lead developer; null when unassigned. | | `data.lead_qa_id` | string \| null | yes | The QA lead; null when unassigned. | | `data.starts_on` | string \| null | yes | The planned start, `YYYY-MM-DD`; null when not set. | | `data.target_launch_on` | string \| null | yes | The target launch date, `YYYY-MM-DD`; null when not set. | | `data.launched_on` | string \| null | yes | The day the project went live, `YYYY-MM-DD`; null until it did. | | `data.budget_hours` | integer \| null | yes | The budgeted effort in hours; null when none was agreed. | | `data.archived_at` | string \| null | yes | When the project was archived, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the project was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the project last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder may not change the project. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when `client_id` is not one of the team's clients. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, the key or the name is taken, the key changes after an artefact carries it, `status` is `archived`, or a lead is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a RAID item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/add-a-raid-item `POST /v1/projects/{project}/raid-items` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/raid-items \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "title": "Payment provider sandbox may lag behind production", "type": "risk", "category": "external", "probability": 3, "impact": 4, "mitigation": "

Run the checkout smoke test against the provider'"'"'s production account every Monday.

", "review_on": "2026-11-16" }' ``` Only `title` is required. The item takes the next number of the log's sequence whatever its kind, is raised by the token's holder, starts `open` unless `status` says otherwise, as a `risk` unless `type` does and in the area `other` unless `category` does. The owner must be a member of the team. A column the kind does not carry is dropped: a probability on an issue, a mitigation on an assumption, a due date on a risk, a review date on a dependency. The save writes version one of the item. Answers `201` with the item and emits `raid_item.created`. - Requires ability: `raid:create` - Fires events: `raid_item.created` - MCP tools: `raise_risk` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A RAID item to add or change. Only `title` is required on a create; the kind defaults to `risk`, the area to `other` and the status to `open`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves through its own endpoint. A column the kind does not carry (a probability on an issue, a due date on a risk) is dropped. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the item is called, 3 to 200 characters. | | `type` | `risk`, `assumption`, `issue`, `dependency` | no | The kind: `risk`, `assumption`, `issue` or `dependency`. Defaults to `risk`. | | `description` | string \| null | no | What it is, as HTML; filtered to a safe subset when read. | | `category` | `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality`, `other` | no | The area it touches: `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality` or `other`. Defaults to `other`. | | `probability` | integer \| null | no | How likely, one to five; kept for a risk only. | | `impact` | integer \| null | no | How bad, one to five; kept for a risk or an issue only. | | `mitigation` | string \| null | no | What makes a risk less likely or less bad, as HTML; kept for a risk only. | | `contingency` | string \| null | no | What happens if a risk occurs, as HTML; kept for a risk only. | | `owner_id` | string \| null (uuid) | no | The id of the person who answers for the item, a member of the team; null for nobody. | | `due_on` | string \| null (date) | no | When an issue or a dependency must be resolved, `Y-m-d`; kept for those kinds only. | | `review_on` | string \| null (date) | no | When a risk or an assumption is next reviewed, `Y-m-d`; kept for those kinds only. | | `depends_on` | string \| null | no | What the project depends on, in words up to 200 characters; kept for a dependency only. | | `status` | `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred`, `accepted` | no | Where it starts, on a create only: `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`. Defaults to `open`. | ### Responses - **201**: The item, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the log's own sequence, shared by every kind. | | `data.reference` | string | yes | The key people use, such as `CRM-RSK-4`, whatever the kind. | | `data.type` | string | yes | The kind: `risk`, `assumption`, `issue` or `dependency`. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.category` | string | yes | The area it touches: `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality` or `other`. | | `data.probability` | integer \| null | yes | How likely, one to five; null unless the item is a risk. | | `data.impact` | integer \| null | yes | How bad, one to five; null unless the item is a risk or an issue. | | `data.score` | integer \| null | yes | Probability times impact for a risk, the impact for an issue; null for the other kinds. | | `data.mitigation` | string \| null | yes | What makes a risk less likely or less bad, as HTML; null unless the item is a risk. | | `data.contingency` | string \| null | yes | What happens if a risk occurs, as HTML; null unless the item is a risk. | | `data.owner_id` | string \| null | yes | The person who answers for the item; null when nobody does. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.raised_by` | string \| null | yes | The person who raised the item; null when the account is gone. | | `data.raised_by_name` | string \| null | yes | That person's name, when the raiser was loaded; null otherwise. | | `data.due_on` | string \| null | yes | When an issue or a dependency must be resolved, `Y-m-d`; null for the other kinds or when unset. | | `data.review_on` | string \| null | yes | When a risk or an assumption is next reviewed, `Y-m-d`; null for the other kinds or when unset. | | `data.depends_on` | string \| null | yes | What the project depends on, in words; null unless the item is a dependency. | | `data.status` | string | yes | Where it stands: `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`. | | `data.position` | integer | yes | Its place in the project's log, from zero. | | `data.created_at` | string \| null | yes | When the item was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `raid:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, a rating is outside one to five, or the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a RAID item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/delete-a-raid-item `DELETE /v1/raid-items/{raidItem}` ```bash curl -X DELETE https://api.example.test/v1/raid-items/$RAID_ITEM_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes an item with its versions and its activity history. The number it took is never handed out again. Returns `204 No Content` and emits `raid_item.deleted`. Prefer a move to `closed` for an item that no longer matters; it keeps the record. - Requires ability: `raid:delete` - Fires events: `raid_item.deleted` - MCP tools: `delete_raid_item` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `raidItem` | path | string (uuid) | yes | The item, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `raid:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a RAID item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-raid-item `GET /v1/raid-items/{raidItem}` ```bash curl https://api.example.test/v1/raid-items/$RAID_ITEM_ID \ -H "Authorization: Bearer $TOKEN" ``` One item with its owner's and its raiser's names. An item of a project the token may not see, or any item read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `type` is `risk`, `assumption`, `issue` or `dependency`; `status` is `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`; `probability` is kept for a risk and `impact` for a risk or an issue, each from one to five, and `score` is their product for a risk and the impact for an issue. - Requires ability: `raid:view` - MCP tools: `get_raid_item` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `raidItem` | path | string (uuid) | yes | The item, bound by the middleware. | ### Responses - **200**: The item with its owner and raiser. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the log's own sequence, shared by every kind. | | `data.reference` | string | yes | The key people use, such as `CRM-RSK-4`, whatever the kind. | | `data.type` | string | yes | The kind: `risk`, `assumption`, `issue` or `dependency`. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.category` | string | yes | The area it touches: `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality` or `other`. | | `data.probability` | integer \| null | yes | How likely, one to five; null unless the item is a risk. | | `data.impact` | integer \| null | yes | How bad, one to five; null unless the item is a risk or an issue. | | `data.score` | integer \| null | yes | Probability times impact for a risk, the impact for an issue; null for the other kinds. | | `data.mitigation` | string \| null | yes | What makes a risk less likely or less bad, as HTML; null unless the item is a risk. | | `data.contingency` | string \| null | yes | What happens if a risk occurs, as HTML; null unless the item is a risk. | | `data.owner_id` | string \| null | yes | The person who answers for the item; null when nobody does. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.raised_by` | string \| null | yes | The person who raised the item; null when the account is gone. | | `data.raised_by_name` | string \| null | yes | That person's name, when the raiser was loaded; null otherwise. | | `data.due_on` | string \| null | yes | When an issue or a dependency must be resolved, `Y-m-d`; null for the other kinds or when unset. | | `data.review_on` | string \| null | yes | When a risk or an assumption is next reviewed, `Y-m-d`; null for the other kinds or when unset. | | `data.depends_on` | string \| null | yes | What the project depends on, in words; null unless the item is a dependency. | | `data.status` | string | yes | Where it stands: `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`. | | `data.position` | integer | yes | Its place in the project's log, from zero. | | `data.created_at` | string \| null | yes | When the item was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a version of a RAID item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-version-of-a-raid-item `GET /v1/raid-items/{raidItem}/revisions/{revision}` ```bash curl https://api.example.test/v1/raid-items/$RAID_ITEM_ID/revisions/$REVISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One version of the item. A version of another artefact, a version the token may not see or a client guest's token is `404 RESOURCE_NOT_FOUND`. - Requires ability: `raid:view` - MCP tools: `get_raid_item` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `raidItem` | path | string (uuid) | yes | The item, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # RAID Log API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log The **RAID log** is where a project writes down what may go wrong and what it leans on: **risks** (something that may happen and would hurt, scored by probability and impact, with a mitigation and a contingency), **assumptions** (something taken as true for the plan to hold, reviewed until it is confirmed or fails), **issues** (something that has happened and hurts now, scored by impact) and **dependencies** (something the project needs from outside itself). The four kinds share one sequence (`CRM-RSK-4`) and one table, because the review meeting walks them together and an assumption that fails becomes an issue without changing its key. Each item carries the area it touches, who owns it and who raised it, when it is due or next reviewed, and a status from open through monitoring, mitigating, resolved, closed, occurred and accepted. Every save of an item writes a **version**; the versions are read and put back through the endpoints below, under the log's own abilities. The log is internal. A staff token sees the items of the projects it may see; a client guest's token sees none of them, and an item it asks for is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the RAID Log page runs, under the project permissions, and announces itself as a `raid_item.*` event. ## Endpoints - `GET /v1/projects/{project}/raid-items` — [List RAID items](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-raid-items) - `POST /v1/projects/{project}/raid-items` — [Add a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/add-a-raid-item) - `GET /v1/raid-items/{raidItem}` — [Get a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-raid-item) - `PATCH /v1/raid-items/{raidItem}` — [Update a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/update-a-raid-item) - `DELETE /v1/raid-items/{raidItem}` — [Delete a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/delete-a-raid-item) - `GET /v1/raid-items/{raidItem}/revisions` — [List a RAID item's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-a-raid-items-versions) - `GET /v1/raid-items/{raidItem}/revisions/{revision}` — [Get a version of a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-version-of-a-raid-item) - `POST /v1/raid-items/{raidItem}/status` — [Move a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/move-a-raid-item) - `POST /v1/raid-items/{raidItem}/revisions/{revision}/restore` — [Restore a version of a RAID item](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/restore-a-version-of-a-raid-item) ## Related - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items): The stories and tasks a risk's contingency or an issue's fix becomes, once links land. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): Who changed an item and when, beside the versions themselves. - [raid_item.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item): The lifecycle of a project's RAID log: a risk, an assumption, an issue or a dependency raised, changed, moved along its lifecycle, put back to an earlier version and deleted. --- # List a RAID item's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-a-raid-items-versions `GET /v1/raid-items/{raidItem}/revisions` ```bash curl "https://api.example.test/v1/raid-items/$RAID_ITEM_ID/revisions" \ -H "Authorization: Bearer $TOKEN" ``` Every version of the item, newest first, 25 per page (`per_page` 1 to 100). Each carries its `version` number, the `reason` it was written, who wrote it (`author_id`, `author_name`) and through what (`actor_kind`), a one-line `summary`, the `changed_fields`, the `diff` from the version before and the full `snapshot`. A client guest's token is refused. - Requires ability: `raid:view` - MCP tools: `get_raid_item` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `raidItem` | path | string (uuid) | yes | The item, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role does not carry `raid:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List RAID items Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-raid-items `GET /v1/projects/{project}/raid-items` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/raid-items?type=risk&status=open" \ -H "Authorization: Bearer $TOKEN" ``` The items of one project in the project's order, 25 per page (`per_page` 1 to 100), each with its owner's and its raiser's names. `sort_by` takes `number`, `reference`, `title`, `type`, `category`, `status`, `score`, `probability`, `impact`, `due_on`, `review_on`, `position`, `created_at` or `updated_at`. `type`, `status`, `category` and `owner_id` narrow the page to one value each, and `search` to the items whose reference, title or dependency contain the words. - Requires ability: `raid:view-any` - MCP tools: `list_raid_items` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `type` | query | `risk`, `assumption`, `issue`, `dependency` \| null | no | Narrow to one kind: `risk`, `assumption`, `issue` or `dependency`. An unknown kind is refused. | | `status` | query | `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred`, `accepted` \| null | no | Narrow to one status: `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`. | | `category` | query | `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality`, `other` \| null | no | Narrow to one area: `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality` or `other`. | | `owner_id` | query | string \| null (uuid) | no | Narrow to the items one person owns. | | `search` | query | string \| null | no | Words the reference, the title or what the item depends on must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of items with their owners and raisers. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The item's id. | | `data[].project_id` | string | yes | The project the item belongs to. | | `data[].number` | integer | yes | Its number in the log's own sequence, shared by every kind. | | `data[].reference` | string | yes | The key people use, such as `CRM-RSK-4`, whatever the kind. | | `data[].type` | string | yes | The kind: `risk`, `assumption`, `issue` or `dependency`. | | `data[].title` | string | yes | What the item is called. | | `data[].description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data[].category` | string | yes | The area it touches: `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality` or `other`. | | `data[].probability` | integer \| null | yes | How likely, one to five; null unless the item is a risk. | | `data[].impact` | integer \| null | yes | How bad, one to five; null unless the item is a risk or an issue. | | `data[].score` | integer \| null | yes | Probability times impact for a risk, the impact for an issue; null for the other kinds. | | `data[].mitigation` | string \| null | yes | What makes a risk less likely or less bad, as HTML; null unless the item is a risk. | | `data[].contingency` | string \| null | yes | What happens if a risk occurs, as HTML; null unless the item is a risk. | | `data[].owner_id` | string \| null | yes | The person who answers for the item; null when nobody does. | | `data[].owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data[].raised_by` | string \| null | yes | The person who raised the item; null when the account is gone. | | `data[].raised_by_name` | string \| null | yes | That person's name, when the raiser was loaded; null otherwise. | | `data[].due_on` | string \| null | yes | When an issue or a dependency must be resolved, `Y-m-d`; null for the other kinds or when unset. | | `data[].review_on` | string \| null | yes | When a risk or an assumption is next reviewed, `Y-m-d`; null for the other kinds or when unset. | | `data[].depends_on` | string \| null | yes | What the project depends on, in words; null unless the item is a dependency. | | `data[].status` | string | yes | Where it stands: `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`. | | `data[].position` | integer | yes | Its place in the project's log, from zero. | | `data[].created_at` | string \| null | yes | When the item was raised, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the item last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role on the project does not carry `raid:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `type`, `status` or `category` is not one of its values, `owner_id` is not a uuid, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a RAID item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/move-a-raid-item `POST /v1/raid-items/{raidItem}/status` ```bash curl -X POST https://api.example.test/v1/raid-items/$RAID_ITEM_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "mitigating"}' ``` Moves the item along its lifecycle: a closed item goes back to `open` only, and every other state may move to any other. The move writes a version with the reason `transition`. Answers `200` with the item and emits `raid_item.status_changed` with the status before and after; a move to the status the item is in already answers `200` and emits nothing. - Requires ability: `raid:update` - Fires events: `raid_item.status_changed` - MCP tools: `update_raid_item` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `raidItem` | path | string (uuid) | yes | The item, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the RAID item to. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred`, `accepted` | yes | Where the item goes: `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`. A closed item goes back to open only; every other move is allowed. | ### Responses - **200**: The item, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the log's own sequence, shared by every kind. | | `data.reference` | string | yes | The key people use, such as `CRM-RSK-4`, whatever the kind. | | `data.type` | string | yes | The kind: `risk`, `assumption`, `issue` or `dependency`. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.category` | string | yes | The area it touches: `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality` or `other`. | | `data.probability` | integer \| null | yes | How likely, one to five; null unless the item is a risk. | | `data.impact` | integer \| null | yes | How bad, one to five; null unless the item is a risk or an issue. | | `data.score` | integer \| null | yes | Probability times impact for a risk, the impact for an issue; null for the other kinds. | | `data.mitigation` | string \| null | yes | What makes a risk less likely or less bad, as HTML; null unless the item is a risk. | | `data.contingency` | string \| null | yes | What happens if a risk occurs, as HTML; null unless the item is a risk. | | `data.owner_id` | string \| null | yes | The person who answers for the item; null when nobody does. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.raised_by` | string \| null | yes | The person who raised the item; null when the account is gone. | | `data.raised_by_name` | string \| null | yes | That person's name, when the raiser was loaded; null otherwise. | | `data.due_on` | string \| null | yes | When an issue or a dependency must be resolved, `Y-m-d`; null for the other kinds or when unset. | | `data.review_on` | string \| null | yes | When a risk or an assumption is next reviewed, `Y-m-d`; null for the other kinds or when unset. | | `data.depends_on` | string \| null | yes | What the project depends on, in words; null unless the item is a dependency. | | `data.status` | string | yes | Where it stands: `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`. | | `data.position` | integer | yes | Its place in the project's log, from zero. | | `data.created_at` | string \| null | yes | When the item was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `raid:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, or when the lifecycle refuses the move. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a RAID item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/restore-a-version-of-a-raid-item `POST /v1/raid-items/{raidItem}/revisions/{revision}/restore` ```bash curl -X POST https://api.example.test/v1/raid-items/$RAID_ITEM_ID/revisions/$REVISION_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the version's captured columns back onto the item, status included, and saves; the score is derived again from the restored probability and impact. The save writes a new version with the reason `restore` that names the one put back in `restored_from_id`; nothing earlier is rewritten. Answers `201` with the version written and emits `raid_item.restored` with both version numbers. - Requires ability: `raid:update` - Fires events: `raid_item.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `raidItem` | path | string (uuid) | yes | The item, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version the restore wrote, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `raid:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the version is not the item's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the version is identical to the current record, so there is nothing to restore. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a RAID item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/update-a-raid-item `PATCH /v1/raid-items/{raidItem}` ```bash curl -X PATCH https://api.example.test/v1/raid-items/$RAID_ITEM_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"probability": 4, "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "review_on": null}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. Changing the kind keeps the reference and drops the columns the new kind does not carry. The score is derived again on every save. The status does not change here; the move endpoint owns it. A save that changes a captured column writes a new version. Answers `200` with the item and emits `raid_item.updated` with the columns that changed, each with its old and new value; an update that changes no column answers `200` and emits nothing. - Requires ability: `raid:update` - Fires events: `raid_item.updated` - MCP tools: `update_raid_item` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `raidItem` | path | string (uuid) | yes | The item, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A RAID item to add or change. Only `title` is required on a create; the kind defaults to `risk`, the area to `other` and the status to `open`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves through its own endpoint. A column the kind does not carry (a probability on an issue, a due date on a risk) is dropped. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the item is called, 3 to 200 characters. | | `type` | `risk`, `assumption`, `issue`, `dependency` | no | The kind: `risk`, `assumption`, `issue` or `dependency`. Defaults to `risk`. | | `description` | string \| null | no | What it is, as HTML; filtered to a safe subset when read. | | `category` | `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality`, `other` | no | The area it touches: `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality` or `other`. Defaults to `other`. | | `probability` | integer \| null | no | How likely, one to five; kept for a risk only. | | `impact` | integer \| null | no | How bad, one to five; kept for a risk or an issue only. | | `mitigation` | string \| null | no | What makes a risk less likely or less bad, as HTML; kept for a risk only. | | `contingency` | string \| null | no | What happens if a risk occurs, as HTML; kept for a risk only. | | `owner_id` | string \| null (uuid) | no | The id of the person who answers for the item, a member of the team; null for nobody. | | `due_on` | string \| null (date) | no | When an issue or a dependency must be resolved, `Y-m-d`; kept for those kinds only. | | `review_on` | string \| null (date) | no | When a risk or an assumption is next reviewed, `Y-m-d`; kept for those kinds only. | | `depends_on` | string \| null | no | What the project depends on, in words up to 200 characters; kept for a dependency only. | | `status` | `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred`, `accepted` | no | Where it starts, on a create only: `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`. Defaults to `open`. | ### Responses - **200**: The item. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the log's own sequence, shared by every kind. | | `data.reference` | string | yes | The key people use, such as `CRM-RSK-4`, whatever the kind. | | `data.type` | string | yes | The kind: `risk`, `assumption`, `issue` or `dependency`. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.category` | string | yes | The area it touches: `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality` or `other`. | | `data.probability` | integer \| null | yes | How likely, one to five; null unless the item is a risk. | | `data.impact` | integer \| null | yes | How bad, one to five; null unless the item is a risk or an issue. | | `data.score` | integer \| null | yes | Probability times impact for a risk, the impact for an issue; null for the other kinds. | | `data.mitigation` | string \| null | yes | What makes a risk less likely or less bad, as HTML; null unless the item is a risk. | | `data.contingency` | string \| null | yes | What happens if a risk occurs, as HTML; null unless the item is a risk. | | `data.owner_id` | string \| null | yes | The person who answers for the item; null when nobody does. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.raised_by` | string \| null | yes | The person who raised the item; null when the account is gone. | | `data.raised_by_name` | string \| null | yes | That person's name, when the raiser was loaded; null otherwise. | | `data.due_on` | string \| null | yes | When an issue or a dependency must be resolved, `Y-m-d`; null for the other kinds or when unset. | | `data.review_on` | string \| null | yes | When a risk or an assumption is next reviewed, `Y-m-d`; null for the other kinds or when unset. | | `data.depends_on` | string \| null | yes | What the project depends on, in words; null unless the item is a dependency. | | `data.status` | string | yes | Where it stands: `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`. | | `data.position` | integer | yes | Its place in the project's log, from zero. | | `data.created_at` | string \| null | yes | When the item was raised, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `raid:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed or the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a repository Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/delete-a-repository `DELETE /v1/repositories/{repository}` ```bash curl -X DELETE https://api.example.test/v1/repositories/$REPOSITORY_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a repository with its activity history; the code itself stays with the provider. Returns `204 No Content` and emits `repository.deleted`. Branches, commits and pull requests that will hang off repositories are not here yet; once they are, a repository with code references will be refused and disconnected instead. - Requires ability: `repository:delete` - Fires events: `repository.deleted` - MCP tools: `delete_repository` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `repository` | path | string (uuid) | yes | The repository, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `repository:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Disconnect a repository Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/disconnect-a-repository `POST /v1/repositories/{repository}/disconnect` ```bash curl -X POST https://api.example.test/v1/repositories/$REPOSITORY_ID/disconnect \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Sets the repository aside: it leaves the live listing and the Repositories page's default view while its row and history stay. Answers `200` with the repository, `disconnected_at` set, and emits `repository.disconnected`. - Requires ability: `repository:update` - Fires events: `repository.disconnected` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `repository` | path | string (uuid) | yes | The repository, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The repository, disconnected. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The repository's id, the value branches, commits and pull requests will carry. | | `data.project_id` | string | yes | The project the repository belongs to. | | `data.component_id` | string \| null | yes | The component the repository builds; null when it serves the whole project. | | `data.provider` | string | yes | Where it is hosted: `github`, `gitlab` or `bitbucket`. | | `data.owner` | string | yes | The account or group that owns it. | | `data.name` | string | yes | The repository's name. | | `data.full_name` | string | yes | The owner and the name joined with a slash, as the provider spells it; unique per provider within the project. | | `data.default_branch` | string | yes | The branch releases are cut from. | | `data.url` | string | yes | Where the repository lives; the provider's own address unless another was given. | | `data.private` | boolean | yes | Whether the repository is private. | | `data.connected_by` | string \| null | yes | The user who registered it; null when that account is gone. | | `data.connected_at` | string \| null | yes | When it was registered, ISO 8601. | | `data.disconnected_at` | string \| null | yes | When it was set aside, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the row was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `repository:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the repository is disconnected already. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a repository Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/get-a-repository `GET /v1/repositories/{repository}` ```bash curl https://api.example.test/v1/repositories/$REPOSITORY_ID \ -H "Authorization: Bearer $TOKEN" ``` One repository. A repository of a project the token may not see, or any repository read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `provider` is `github`, `gitlab` or `bitbucket`; `full_name` is the owner and the name joined with a slash. - `disconnected_at` is null while the repository is live. - Requires ability: `repository:view` - MCP tools: `get_repository` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `repository` | path | string (uuid) | yes | The repository, bound by the middleware. | ### Responses - **200**: The repository. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The repository's id, the value branches, commits and pull requests will carry. | | `data.project_id` | string | yes | The project the repository belongs to. | | `data.component_id` | string \| null | yes | The component the repository builds; null when it serves the whole project. | | `data.provider` | string | yes | Where it is hosted: `github`, `gitlab` or `bitbucket`. | | `data.owner` | string | yes | The account or group that owns it. | | `data.name` | string | yes | The repository's name. | | `data.full_name` | string | yes | The owner and the name joined with a slash, as the provider spells it; unique per provider within the project. | | `data.default_branch` | string | yes | The branch releases are cut from. | | `data.url` | string | yes | Where the repository lives; the provider's own address unless another was given. | | `data.private` | boolean | yes | Whether the repository is private. | | `data.connected_by` | string \| null | yes | The user who registered it; null when that account is gone. | | `data.connected_at` | string \| null | yes | When it was registered, ISO 8601. | | `data.disconnected_at` | string \| null | yes | When it was set aside, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the row was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Repositories API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories A **repository** is a Git repository a project's code lives in: its provider, the account that owns it, its name, the branch releases are cut from, whether it is private and which component it builds. The registry is kept by hand until Git hosting is connected; from then on the same rows carry the connection and the branches, commits and pull requests that link work items to code. A repository that falls out of use is disconnected, which keeps its row and its history and takes it out of the live list. The reads here see the repositories of the projects the token may see, staff only: a client guest's token reads none, because where the code lives is the team's business. A repository outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Repositories page runs, under the project permissions, and announces itself as a `repository.*` event. ## Endpoints - `GET /v1/repositories/{repository}` — [Get a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/get-a-repository) - `PATCH /v1/repositories/{repository}` — [Update a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/update-a-repository) - `DELETE /v1/repositories/{repository}` — [Delete a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/delete-a-repository) - `GET /v1/projects/{project}/repositories` — [List repositories](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/list-repositories) - `POST /v1/projects/{project}/repositories` — [Register a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/register-a-repository) - `POST /v1/repositories/{repository}/disconnect` — [Disconnect a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/disconnect-a-repository) - `POST /v1/repositories/{repository}/reconnect` — [Reconnect a repository](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/reconnect-a-repository) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project a repository belongs to. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components): The component a repository builds. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on a repository and who did it. - [repository.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository): The lifecycle of a project's repositories: registration, edits, disconnecting, reconnecting and deletion. --- # List repositories Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/list-repositories `GET /v1/projects/{project}/repositories` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/repositories?disconnected=0" \ -H "Authorization: Bearer $TOKEN" ``` The repositories of one project by full name, 25 per page (`per_page` 1 to 100). `sort_by` takes `full_name`, `provider`, `default_branch`, `connected_at`, `created_at` or `updated_at`. `provider` narrows the page to one host, `component_id` to the repositories that build one component, `search` to the repositories whose full name, address or default branch contain the words; `disconnected=1` lists disconnected repositories only and `disconnected=0` live ones only, with both listed when it is omitted. - Requires ability: `repository:view-any` - MCP tools: `list_repositories` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `provider` | query | `github`, `gitlab`, `bitbucket` \| null | no | Narrow to one provider: `github`, `gitlab` or `bitbucket`. An unknown provider is refused. | | `component_id` | query | string \| null (uuid) | no | Narrow to the repositories that build one component. | | `disconnected` | query | boolean \| null | no | `1` lists disconnected repositories only, `0` live ones only; omit for both. | | `search` | query | string \| null | no | Words the full name, the address or the default branch must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of repositories. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The repository's id, the value branches, commits and pull requests will carry. | | `data[].project_id` | string | yes | The project the repository belongs to. | | `data[].component_id` | string \| null | yes | The component the repository builds; null when it serves the whole project. | | `data[].provider` | string | yes | Where it is hosted: `github`, `gitlab` or `bitbucket`. | | `data[].owner` | string | yes | The account or group that owns it. | | `data[].name` | string | yes | The repository's name. | | `data[].full_name` | string | yes | The owner and the name joined with a slash, as the provider spells it; unique per provider within the project. | | `data[].default_branch` | string | yes | The branch releases are cut from. | | `data[].url` | string | yes | Where the repository lives; the provider's own address unless another was given. | | `data[].private` | boolean | yes | Whether the repository is private. | | `data[].connected_by` | string \| null | yes | The user who registered it; null when that account is gone. | | `data[].connected_at` | string \| null | yes | When it was registered, ISO 8601. | | `data[].disconnected_at` | string \| null | yes | When it was set aside, ISO 8601; null while live. | | `data[].created_at` | string \| null | yes | When the row was created, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the row last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder may not read the project's repositories, a client guest among them. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `provider` is not one of its values, `component_id` is not a UUID, `disconnected` is not a boolean or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Reconnect a repository Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/reconnect-a-repository `POST /v1/repositories/{repository}/reconnect` ```bash curl -X POST https://api.example.test/v1/repositories/$REPOSITORY_ID/reconnect \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Brings a disconnected repository back into use. Answers `200` with the repository, `disconnected_at` null again, and emits `repository.reconnected`. - Requires ability: `repository:update` - Fires events: `repository.reconnected` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `repository` | path | string (uuid) | yes | The repository, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The repository, live again. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The repository's id, the value branches, commits and pull requests will carry. | | `data.project_id` | string | yes | The project the repository belongs to. | | `data.component_id` | string \| null | yes | The component the repository builds; null when it serves the whole project. | | `data.provider` | string | yes | Where it is hosted: `github`, `gitlab` or `bitbucket`. | | `data.owner` | string | yes | The account or group that owns it. | | `data.name` | string | yes | The repository's name. | | `data.full_name` | string | yes | The owner and the name joined with a slash, as the provider spells it; unique per provider within the project. | | `data.default_branch` | string | yes | The branch releases are cut from. | | `data.url` | string | yes | Where the repository lives; the provider's own address unless another was given. | | `data.private` | boolean | yes | Whether the repository is private. | | `data.connected_by` | string \| null | yes | The user who registered it; null when that account is gone. | | `data.connected_at` | string \| null | yes | When it was registered, ISO 8601. | | `data.disconnected_at` | string \| null | yes | When it was set aside, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the row was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `repository:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the repository is not disconnected. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Register a repository Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/register-a-repository `POST /v1/projects/{project}/repositories` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/repositories \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "provider": "github", "owner": "zuse-technologies", "name": "customer-portal-web", "default_branch": "main", "component_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f" }' ``` `owner` and `name` are required; `provider` defaults to `github`, `default_branch` to `main` and `private` to true. The address defaults to the provider's own for the owner and name and may be given for a self-managed host. The full name is unique per provider within the project, regardless of case. The token holder is remembered as `connected_by`. Answers `201` with the repository and emits `repository.created`. - Requires ability: `repository:create` - Fires events: `repository.created` - MCP tools: `create_repository` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A repository to register or change. Only `owner` and `name` are required on a create; the provider defaults to `github`, the default branch to `main`, the repository to private, and the address to the provider's own. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `provider` | `github`, `gitlab`, `bitbucket` | no | Where the repository is hosted: `github`, `gitlab` or `bitbucket`. Defaults to `github`. | | `owner` | string | no | The account or group that owns it, up to 100 characters of letters, digits, dots, dashes and underscores. | | `name` | string | no | The repository's name, up to 100 characters of letters, digits, dots, dashes and underscores. | | `url` | string \| null (uri) | no | Where it lives, an `http` or `https` address; leave it out to use the provider's own address for the owner and name. | | `default_branch` | string | no | The branch releases are cut from, without spaces. Defaults to `main`. | | `private` | boolean | no | Whether the repository is private. Defaults to true. | | `component_id` | string \| null (uuid) | no | The component the repository builds, one of the project's; null when it serves the whole project. | ### Responses - **201**: The repository, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The repository's id, the value branches, commits and pull requests will carry. | | `data.project_id` | string | yes | The project the repository belongs to. | | `data.component_id` | string \| null | yes | The component the repository builds; null when it serves the whole project. | | `data.provider` | string | yes | Where it is hosted: `github`, `gitlab` or `bitbucket`. | | `data.owner` | string | yes | The account or group that owns it. | | `data.name` | string | yes | The repository's name. | | `data.full_name` | string | yes | The owner and the name joined with a slash, as the provider spells it; unique per provider within the project. | | `data.default_branch` | string | yes | The branch releases are cut from. | | `data.url` | string | yes | Where the repository lives; the provider's own address unless another was given. | | `data.private` | boolean | yes | Whether the repository is private. | | `data.connected_by` | string \| null | yes | The user who registered it; null when that account is gone. | | `data.connected_at` | string \| null | yes | When it was registered, ISO 8601. | | `data.disconnected_at` | string \| null | yes | When it was set aside, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the row was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `repository:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when `component_id` is not a component of the project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when the owner or the name carries a slash, a space or another character the providers refuse, or when the project already registers that full name with that provider. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a repository Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/update-a-repository `PATCH /v1/repositories/{repository}` ```bash curl -X PATCH https://api.example.test/v1/repositories/$REPOSITORY_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"default_branch": "release/2026.11", "private": false}' ``` Send only the fields to change: a field left out keeps its value, and `url` or `component_id` sent as `null` is cleared, the address falling back to the provider's own. A new owner or name must leave the full name free within the project for the provider. Answers `200` with the repository and emits `repository.updated` with the columns that changed, each with its old and new value; an update that changes nothing answers `200` and emits nothing. - Requires ability: `repository:update` - Fires events: `repository.updated` - MCP tools: `update_repository` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `repository` | path | string (uuid) | yes | The repository, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A repository to register or change. Only `owner` and `name` are required on a create; the provider defaults to `github`, the default branch to `main`, the repository to private, and the address to the provider's own. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `provider` | `github`, `gitlab`, `bitbucket` | no | Where the repository is hosted: `github`, `gitlab` or `bitbucket`. Defaults to `github`. | | `owner` | string | no | The account or group that owns it, up to 100 characters of letters, digits, dots, dashes and underscores. | | `name` | string | no | The repository's name, up to 100 characters of letters, digits, dots, dashes and underscores. | | `url` | string \| null (uri) | no | Where it lives, an `http` or `https` address; leave it out to use the provider's own address for the owner and name. | | `default_branch` | string | no | The branch releases are cut from, without spaces. Defaults to `main`. | | `private` | boolean | no | Whether the repository is private. Defaults to true. | | `component_id` | string \| null (uuid) | no | The component the repository builds, one of the project's; null when it serves the whole project. | ### Responses - **200**: The repository. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The repository's id, the value branches, commits and pull requests will carry. | | `data.project_id` | string | yes | The project the repository belongs to. | | `data.component_id` | string \| null | yes | The component the repository builds; null when it serves the whole project. | | `data.provider` | string | yes | Where it is hosted: `github`, `gitlab` or `bitbucket`. | | `data.owner` | string | yes | The account or group that owns it. | | `data.name` | string | yes | The repository's name. | | `data.full_name` | string | yes | The owner and the name joined with a slash, as the provider spells it; unique per provider within the project. | | `data.default_branch` | string | yes | The branch releases are cut from. | | `data.url` | string | yes | Where the repository lives; the provider's own address unless another was given. | | `data.private` | boolean | yes | Whether the repository is private. | | `data.connected_by` | string \| null | yes | The user who registered it; null when that account is gone. | | `data.connected_at` | string \| null | yes | When it was registered, ISO 8601. | | `data.disconnected_at` | string \| null | yes | When it was set aside, ISO 8601; null while live. | | `data.created_at` | string \| null | yes | When the row was created, ISO 8601. | | `data.updated_at` | string \| null | yes | When the row last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `repository:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when `component_id` is not a component of the project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed or when the new full name is already registered with that provider on the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a requirement Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/add-a-requirement `POST /v1/projects/{project}/requirements` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/requirements \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "title": "Tier page answers within a third of a second", "type": "non_functional", "category": "performance", "statement": "

The system shall render a member'"'"'s tier page within 300 ms at the 95th percentile with 500 members browsing at once.

", "measure": "p95 under 300 ms at 500 concurrent users, measured on staging", "priority": "must", "verification_method": "test" }' ``` Only `title` is required. The requirement takes the next number of its kind's sequence and the reference spelled from the project's key and the kind's infix, starts as a `draft` unless `status` says otherwise, `functional` unless `type` does, `should` unless `priority` does and `test` unless `verification_method` does. A non-functional requirement needs a `category`; a functional one ignores `category` and `measure`. The source stakeholder must be a stakeholder of the project and the owner a member of the team. The save writes version one. Answers `201` with the requirement and emits `requirement.created`. - Requires ability: `requirement:create` - Fires events: `requirement.created` - MCP tools: `create_requirement_draft` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A requirement to add or change. Only `title` is required on a create; the kind defaults to `functional`, the priority to `should`, the verification method to `test` and the status to `draft`. A non-functional requirement needs a `category`. On an update, a field left out keeps its value and a field sent as null is cleared; the kind is fixed and the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the requirement is called, 3 to 200 characters. | | `type` | `functional`, `non_functional` | no | The kind, on a create only: `functional` or `non_functional`. Defaults to `functional` and is fixed once the requirement is numbered. | | `category` | `performance`, `scalability`, `availability`, `reliability`, `security`, `privacy`, `usability`, `accessibility`, `compatibility`, `maintainability`, `portability`, `compliance`, `observability`, `localisation`, `other` \| null | no | The quality a non-functional requirement is about: `performance`, `scalability`, `availability`, `reliability`, `security`, `privacy`, `usability`, `accessibility`, `compatibility`, `maintainability`, `portability`, `compliance`, `observability`, `localisation` or `other`. Required for a non-functional requirement; ignored for a functional one. | | `statement` | string \| null | no | What the system shall do or keep, as HTML; filtered to a safe subset when read. | | `rationale` | string \| null | no | Why, as HTML. | | `source_stakeholder_id` | string \| null (uuid) | no | The id of the stakeholder it came from, a stakeholder of the same project; null for none. | | `source` | string \| null | no | Where it came from, in words, up to 300 characters. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority: `must`, `should`, `could` or `wont`. Defaults to `should`. | | `measure` | string \| null | no | The metric and target that say when a non-functional requirement is met, up to 500 characters; ignored for a functional one. | | `verification_method` | `test`, `inspection`, `analysis`, `demonstration` | no | How it is shown to be met: `test`, `inspection`, `analysis` or `demonstration`. Defaults to `test`. | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | no | Where it starts, on a create only: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. Defaults to `draft`. | | `owner_id` | string \| null (uuid) | no | The id of the person who answers for it, a member of the team; null for nobody. | ### Responses - **201**: The requirement, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The requirement's id. | | `data.project_id` | string | yes | The project the requirement belongs to. | | `data.type` | string | yes | The kind: `functional` or `non_functional`. Fixed once the requirement is numbered. | | `data.number` | integer | yes | Its number in the kind's own sequence. | | `data.reference` | string | yes | The key stories, test cases and people use, such as `CRM-FR-12` or `CRM-NFR-3`. | | `data.category` | string \| null | yes | The quality a non-functional requirement is about, such as `performance` or `security`; null for a functional one. | | `data.title` | string | yes | What the requirement is called. | | `data.statement` | string \| null | yes | What the system shall do or keep, as HTML filtered to a safe subset; null when none. | | `data.rationale` | string \| null | yes | Why, as HTML; null when none. | | `data.source_stakeholder_id` | string \| null | yes | The stakeholder it came from; null when none. | | `data.source_stakeholder_name` | string \| null | yes | That stakeholder's name, when the source was loaded; null otherwise. | | `data.source` | string \| null | yes | Where it came from, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.measure` | string \| null | yes | The metric and target that say when a non-functional requirement is met; null for a functional one or when not set. | | `data.verification_method` | string | yes | How it is shown to be met: `test`, `inspection`, `analysis` or `demonstration`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's requirements, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the requirement was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the requirement last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `requirement:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when a non-functional requirement has no category, when the source is not a stakeholder of the project, or when the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a requirement Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/delete-a-requirement `DELETE /v1/requirements/{requirement}` ```bash curl -X DELETE https://api.example.test/v1/requirements/$REQUIREMENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a requirement with its versions and its activity history. The number it took is never handed out again. Returns `204 No Content` and emits `requirement.deleted`. Prefer a move to `deprecated` for a requirement the client withdrew; it keeps the record. - Requires ability: `requirement:delete` - Fires events: `requirement.deleted` - MCP tools: `delete_requirement` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `requirement:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a requirement Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-requirement `GET /v1/requirements/{requirement}` ```bash curl https://api.example.test/v1/requirements/$REQUIREMENT_ID \ -H "Authorization: Bearer $TOKEN" ``` One requirement with its owner's and its source stakeholder's names. A requirement of a project the token may not see, or a draft read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `type` is `functional` or `non_functional`; `category` and `measure` are set for a non-functional requirement only; `status` is `draft`, `in_review`, `approved`, `baselined` or `deprecated`; `priority` is `must`, `should`, `could` or `wont`; `verification_method` is `test`, `inspection`, `analysis` or `demonstration`. - Requires ability: `requirement:view` - MCP tools: `get_requirement` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement, bound by the middleware. | ### Responses - **200**: The requirement. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The requirement's id. | | `data.project_id` | string | yes | The project the requirement belongs to. | | `data.type` | string | yes | The kind: `functional` or `non_functional`. Fixed once the requirement is numbered. | | `data.number` | integer | yes | Its number in the kind's own sequence. | | `data.reference` | string | yes | The key stories, test cases and people use, such as `CRM-FR-12` or `CRM-NFR-3`. | | `data.category` | string \| null | yes | The quality a non-functional requirement is about, such as `performance` or `security`; null for a functional one. | | `data.title` | string | yes | What the requirement is called. | | `data.statement` | string \| null | yes | What the system shall do or keep, as HTML filtered to a safe subset; null when none. | | `data.rationale` | string \| null | yes | Why, as HTML; null when none. | | `data.source_stakeholder_id` | string \| null | yes | The stakeholder it came from; null when none. | | `data.source_stakeholder_name` | string \| null | yes | That stakeholder's name, when the source was loaded; null otherwise. | | `data.source` | string \| null | yes | Where it came from, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.measure` | string \| null | yes | The metric and target that say when a non-functional requirement is met; null for a functional one or when not set. | | `data.verification_method` | string | yes | How it is shown to be met: `test`, `inspection`, `analysis` or `demonstration`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's requirements, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the requirement was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the requirement last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a version of a requirement Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-version-of-a-requirement `GET /v1/requirements/{requirement}/revisions/{revision}` ```bash curl https://api.example.test/v1/requirements/$REQUIREMENT_ID/revisions/$REVISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One version of the requirement. A version of another artefact, a version the token may not see or a client guest's token is `404 RESOURCE_NOT_FOUND`. - Requires ability: `requirement:view` - MCP tools: `get_requirement` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Requirements API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements A **requirement** is one sentence the client and the team agree the system must honour: a **functional** requirement says what it does for a person (`CRM-FR-12`), a **non-functional** one says a quality it keeps while it does it (`CRM-NFR-3`), with the category the quality belongs to and a measure with the metric and the target. Each kind numbers on its own sequence, so the kind is fixed the moment a requirement is numbered. The record carries the statement and the rationale, where the requirement came from (a stakeholder of the project, or words), its MoSCoW priority, how it will be shown to be met, who answers for it and where it stands, from draft through review to approved, baselined or deprecated. Every save of a requirement writes a **version**. The versions are read and put back through the endpoints below, under the requirement's own abilities. The reads here see the requirements of the projects the token may see; a client guest's token sees the approved and baselined ones only, because a draft is the analyst's working note until the client signs it off. A requirement outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Requirements page runs, under the project permissions, and announces itself as a `requirement.*` event. ## Endpoints - `GET /v1/requirements/{requirement}` — [Get a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-requirement) - `PATCH /v1/requirements/{requirement}` — [Update a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/update-a-requirement) - `DELETE /v1/requirements/{requirement}` — [Delete a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/delete-a-requirement) - `GET /v1/requirements/{requirement}/revisions` — [List a requirement's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-versions) - `GET /v1/requirements/{requirement}/revisions/{revision}` — [Get a version of a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-version-of-a-requirement) - `GET /v1/requirements/{requirement}/quality-reviews` — [List a requirement's quality reviews](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-quality-reviews) - `POST /v1/requirements/{requirement}/quality-reviews` — [Review a requirement's quality](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/review-a-requirements-quality) - `GET /v1/projects/{project}/requirements` — [List requirements](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-requirements) - `POST /v1/projects/{project}/requirements` — [Add a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/add-a-requirement) - `POST /v1/requirements/{requirement}/status` — [Move a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/move-a-requirement) - `POST /v1/requirements/{requirement}/revisions/{revision}/restore` — [Restore a version of a requirement](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/restore-a-version-of-a-requirement) ## Related - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders): The people a requirement can be sourced from. - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics): And [work items](/api/v1/reference/work-items): the scope and the stories that will implement the requirements once links land. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): Who changed a requirement and when, beside the versions themselves. - [requirement.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement): The lifecycle of a project's requirements: a functional or non-functional requirement added, changed, moved along its review lifecycle, put back to an earlier version and deleted. --- # List a requirement's quality reviews Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-quality-reviews `GET /v1/requirements/{requirement}/quality-reviews` ```bash curl https://api.example.test/v1/requirements/{requirement}/quality-reviews \ -H "Authorization: Bearer $TOKEN" ``` Every review Heimdall wrote of the requirement, newest first, each with the version it read, its score and band, its summary and its findings. Compare the first review's `version` with the requirement's current version from its history: a review of an older version may be stale. A client guest's token is refused. - Requires ability: `requirement:view` - MCP tools: `list_quality_reviews` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement, bound by the middleware. | ### Responses - **200**: Array of `QualityReviewResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The review's id. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].artefact_type` | string | yes | The artefact's type: `work-item`, `requirement` or `business-rule`. | | `data[].artefact_id` | string | yes | The artefact's id. | | `data[].version` | integer \| null | yes | The version of the artefact Heimdall read, as its history numbers them; null when the artefact had no version yet. | | `data[].score` | integer | yes | 0 to 100: 100 less each finding's penalty (25 for `high`, 10 for `medium`, 4 for `low`), never below 0. | | `data[].band` | string | yes | What the score means: `ready` at 80 and above, `needs_work` from 50 to 79, `not_ready` below 50. | | `data[].summary` | string \| null | yes | Two or three sentences on the record as a whole; null when the model gave none. | | `data[].findings` | array of object | yes | The findings, the most serious first; empty when the record was found sound. | | `data[].findings[].code` | string | yes | The kind of problem: `independent`, `negotiable`, `valuable`, `estimable`, `small` or `testable` for an INVEST breach, `ambiguous`, `missing_criteria`, `untestable`, `missing_measure`, `missing_rationale`, `missing_examples`, `incomplete`, `rule_conflict`, `duplicate`, `stack_drift` or `other`. | | `data[].findings[].severity` | string | yes | How much it matters: `high` (a developer or a tester would be blocked or misled), `medium` (usable but would be argued over) or `low` (polish). | | `data[].findings[].message` | string | yes | What is wrong or missing, quoting the words concerned. | | `data[].findings[].suggestion` | string \| null | yes | What to write instead, in the record's own words; null when there is nothing to suggest. | | `data[].findings[].field` | string \| null | yes | The field concerned, such as `title`, `description`, `criteria`, `statement` or `measure`; null for the whole record. | | `data[].findings_count` | integer | yes | How many findings the review carries. | | `data[].provider` | string | yes | The provider that answered, by its configuration key. | | `data[].model` | string | yes | The model that answered. | | `data[].usage_id` | string \| null | yes | The ledger row that filed the call's cost; null when that row is gone. | | `data[].reviewed_by` | string \| null | yes | The person who asked; null when the account is gone. | | `data[].reviewer_name` | string \| null | yes | That person's name, when the reviewer was loaded; null otherwise. | | `data[].reviewed_at` | string | yes | When Heimdall answered, ISO 8601. | | `data[].created_at` | string \| null | yes | When the review was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `requirement:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the requirement is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List a requirement's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-versions `GET /v1/requirements/{requirement}/revisions` ```bash curl "https://api.example.test/v1/requirements/$REQUIREMENT_ID/revisions" \ -H "Authorization: Bearer $TOKEN" ``` Every version of the requirement, newest first, 25 per page (`per_page` 1 to 100). Each carries its `version` number, the `reason` it was written, who wrote it (`author_id`, `author_name`) and through what (`actor_kind`), a one-line `summary`, the `changed_fields`, the `diff` from the version before and the full `snapshot`. A client guest's token is refused. - Requires ability: `requirement:view` - MCP tools: `get_requirement` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role does not carry `requirement:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List requirements Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-requirements `GET /v1/projects/{project}/requirements` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/requirements?type=non_functional&status=approved" \ -H "Authorization: Bearer $TOKEN" ``` The requirements of one project in the project's order, 25 per page (`per_page` 1 to 100), each with its owner's and its source stakeholder's names. `sort_by` takes `number`, `reference`, `title`, `type`, `category`, `status`, `priority`, `verification_method`, `position`, `created_at` or `updated_at`. `type`, `category`, `status`, `priority` and `owner_id` narrow the page to one value each, and `search` to the requirements whose reference, title or source contain the words. - Requires ability: `requirement:view-any` - MCP tools: `list_requirements` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `type` | query | `functional`, `non_functional` \| null | no | Narrow to one kind: `functional` or `non_functional`. | | `category` | query | `performance`, `scalability`, `availability`, `reliability`, `security`, `privacy`, `usability`, `accessibility`, `compatibility`, `maintainability`, `portability`, `compliance`, `observability`, `localisation`, `other` \| null | no | Narrow to the non-functional requirements of one category, such as `performance` or `security`. | | `status` | query | `draft`, `in_review`, `approved`, `baselined`, `deprecated` \| null | no | Narrow to one status: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. An unknown status is refused. | | `priority` | query | `must`, `should`, `could`, `wont` \| null | no | Narrow to one MoSCoW priority: `must`, `should`, `could` or `wont`. | | `owner_id` | query | string \| null (uuid) | no | Narrow to the requirements one person answers for. | | `search` | query | string \| null | no | Words the reference, the title or the source must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of requirements. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The requirement's id. | | `data[].project_id` | string | yes | The project the requirement belongs to. | | `data[].type` | string | yes | The kind: `functional` or `non_functional`. Fixed once the requirement is numbered. | | `data[].number` | integer | yes | Its number in the kind's own sequence. | | `data[].reference` | string | yes | The key stories, test cases and people use, such as `CRM-FR-12` or `CRM-NFR-3`. | | `data[].category` | string \| null | yes | The quality a non-functional requirement is about, such as `performance` or `security`; null for a functional one. | | `data[].title` | string | yes | What the requirement is called. | | `data[].statement` | string \| null | yes | What the system shall do or keep, as HTML filtered to a safe subset; null when none. | | `data[].rationale` | string \| null | yes | Why, as HTML; null when none. | | `data[].source_stakeholder_id` | string \| null | yes | The stakeholder it came from; null when none. | | `data[].source_stakeholder_name` | string \| null | yes | That stakeholder's name, when the source was loaded; null otherwise. | | `data[].source` | string \| null | yes | Where it came from, in words; null when none. | | `data[].priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data[].measure` | string \| null | yes | The metric and target that say when a non-functional requirement is met; null for a functional one or when not set. | | `data[].verification_method` | string | yes | How it is shown to be met: `test`, `inspection`, `analysis` or `demonstration`. | | `data[].status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data[].owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data[].owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data[].position` | integer | yes | Its place among the project's requirements, from zero. | | `data[].version` | integer | yes | How many times it was approved; zero until the first approval. | | `data[].approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data[].approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data[].created_at` | string \| null | yes | When the requirement was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the requirement last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `type`, `category`, `status` or `priority` is not one of its values, `owner_id` is not a uuid, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a requirement Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/move-a-requirement `POST /v1/requirements/{requirement}/status` ```bash curl -X POST https://api.example.test/v1/requirements/$REQUIREMENT_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "in_review"}' ``` Moves the requirement along its lifecycle. A draft goes to `in_review` or `deprecated`; a review goes back to `draft`, on to `approved` or to `deprecated`; an approved requirement goes back to `in_review`, on to `baselined` or to `deprecated`; a baselined one is reopened to `in_review` or goes to `deprecated`; a deprecated one is revived as a `draft`. A move into `approved` stamps when the requirement was agreed, and a client guest reads it from that moment on, through baselined, and loses it when it is deprecated. The move writes a version with the reason `transition`. Answers `200` with the requirement and emits `requirement.status_changed` with the status before and after; a move to the status the requirement is in already answers `200` and emits nothing. - Requires ability: `requirement:update` - Fires events: `requirement.status_changed` - MCP tools: `update_requirement` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the requirement to. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | yes | Where the requirement goes: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. The move must be one the lifecycle allows from the current status. | ### Responses - **200**: The requirement, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The requirement's id. | | `data.project_id` | string | yes | The project the requirement belongs to. | | `data.type` | string | yes | The kind: `functional` or `non_functional`. Fixed once the requirement is numbered. | | `data.number` | integer | yes | Its number in the kind's own sequence. | | `data.reference` | string | yes | The key stories, test cases and people use, such as `CRM-FR-12` or `CRM-NFR-3`. | | `data.category` | string \| null | yes | The quality a non-functional requirement is about, such as `performance` or `security`; null for a functional one. | | `data.title` | string | yes | What the requirement is called. | | `data.statement` | string \| null | yes | What the system shall do or keep, as HTML filtered to a safe subset; null when none. | | `data.rationale` | string \| null | yes | Why, as HTML; null when none. | | `data.source_stakeholder_id` | string \| null | yes | The stakeholder it came from; null when none. | | `data.source_stakeholder_name` | string \| null | yes | That stakeholder's name, when the source was loaded; null otherwise. | | `data.source` | string \| null | yes | Where it came from, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.measure` | string \| null | yes | The metric and target that say when a non-functional requirement is met; null for a functional one or when not set. | | `data.verification_method` | string | yes | How it is shown to be met: `test`, `inspection`, `analysis` or `demonstration`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's requirements, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the requirement was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the requirement last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `requirement:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, or when the lifecycle refuses the move. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a requirement Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/restore-a-version-of-a-requirement `POST /v1/requirements/{requirement}/revisions/{revision}/restore` ```bash curl -X POST https://api.example.test/v1/requirements/$REQUIREMENT_ID/revisions/$REVISION_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the version's captured columns back onto the requirement, status included, and saves. The save writes a new version with the reason `restore` that names the one put back in `restored_from_id`; nothing earlier is rewritten. Answers `201` with the version written and emits `requirement.restored` with both version numbers. - Requires ability: `requirement:update` - Fires events: `requirement.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version the restore wrote, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `requirement:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the version is not the requirement's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the version is identical to the current record, so there is nothing to restore. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Review a requirement's quality Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/review-a-requirements-quality `POST /v1/requirements/{requirement}/quality-reviews` ```bash curl -X POST https://api.example.test/v1/requirements/{requirement}/quality-reviews \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Asks Heimdall to read the requirement for one testable statement, a rationale, a measure, a fitting verification method, conflicts with the project's business rules and duplicates among its siblings. The review is kept with its findings, the score they leave (100 less 25 per `high`, 10 per `medium` and 4 per `low` finding, never below 0), its band (`ready` at 80 and above, `needs_work` from 50, `not_ready` below) and the version of the requirement it read; the call is filed in the AI ledger under the team's monthly budget. Nothing on the requirement changes. Answers `201` with the review and emits `requirement.quality_reviewed`. Read the latest review first: every call spends. - Requires ability: `ai:use` - Fires events: `requirement.quality_reviewed` - MCP tools: `review_quality` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The review, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The review's id. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.artefact_type` | string | yes | The artefact's type: `work-item`, `requirement` or `business-rule`. | | `data.artefact_id` | string | yes | The artefact's id. | | `data.version` | integer \| null | yes | The version of the artefact Heimdall read, as its history numbers them; null when the artefact had no version yet. | | `data.score` | integer | yes | 0 to 100: 100 less each finding's penalty (25 for `high`, 10 for `medium`, 4 for `low`), never below 0. | | `data.band` | string | yes | What the score means: `ready` at 80 and above, `needs_work` from 50 to 79, `not_ready` below 50. | | `data.summary` | string \| null | yes | Two or three sentences on the record as a whole; null when the model gave none. | | `data.findings` | array of object | yes | The findings, the most serious first; empty when the record was found sound. | | `data.findings[].code` | string | yes | The kind of problem: `independent`, `negotiable`, `valuable`, `estimable`, `small` or `testable` for an INVEST breach, `ambiguous`, `missing_criteria`, `untestable`, `missing_measure`, `missing_rationale`, `missing_examples`, `incomplete`, `rule_conflict`, `duplicate`, `stack_drift` or `other`. | | `data.findings[].severity` | string | yes | How much it matters: `high` (a developer or a tester would be blocked or misled), `medium` (usable but would be argued over) or `low` (polish). | | `data.findings[].message` | string | yes | What is wrong or missing, quoting the words concerned. | | `data.findings[].suggestion` | string \| null | yes | What to write instead, in the record's own words; null when there is nothing to suggest. | | `data.findings[].field` | string \| null | yes | The field concerned, such as `title`, `description`, `criteria`, `statement` or `measure`; null for the whole record. | | `data.findings_count` | integer | yes | How many findings the review carries. | | `data.provider` | string | yes | The provider that answered, by its configuration key. | | `data.model` | string | yes | The model that answered. | | `data.usage_id` | string \| null | yes | The ledger row that filed the call's cost; null when that row is gone. | | `data.reviewed_by` | string \| null | yes | The person who asked; null when the account is gone. | | `data.reviewer_name` | string \| null | yes | That person's name, when the reviewer was loaded; null otherwise. | | `data.reviewed_at` | string | yes | When Heimdall answered, ISO 8601. | | `data.created_at` | string \| null | yes | When the review was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token does not carry `ai:use`, is a client guest's, or the project's assists are off. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the requirement is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `AI_BUDGET_EXHAUSTED`: when the team's monthly AI budget has no room; nothing is filed. `VALIDATION_FAILED`: when Heimdall could not reach its model; nothing is filed or written, and a retry a moment later is safe. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a requirement Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/update-a-requirement `PATCH /v1/requirements/{requirement}` ```bash curl -X PATCH https://api.example.test/v1/requirements/$REQUIREMENT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"priority": "must", "measure": "p95 under 250 ms at 500 concurrent users", "source": null}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. The kind is fixed and `type` is ignored; the status does not change here, the move endpoint owns it. A save that changes a captured column writes a new version. Answers `200` with the requirement and emits `requirement.updated` with the columns that changed, each with its old and new value; an update that changes no column answers `200` and emits nothing. - Requires ability: `requirement:update` - Fires events: `requirement.updated` - MCP tools: `update_requirement` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requirement` | path | string (uuid) | yes | The requirement, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A requirement to add or change. Only `title` is required on a create; the kind defaults to `functional`, the priority to `should`, the verification method to `test` and the status to `draft`. A non-functional requirement needs a `category`. On an update, a field left out keeps its value and a field sent as null is cleared; the kind is fixed and the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the requirement is called, 3 to 200 characters. | | `type` | `functional`, `non_functional` | no | The kind, on a create only: `functional` or `non_functional`. Defaults to `functional` and is fixed once the requirement is numbered. | | `category` | `performance`, `scalability`, `availability`, `reliability`, `security`, `privacy`, `usability`, `accessibility`, `compatibility`, `maintainability`, `portability`, `compliance`, `observability`, `localisation`, `other` \| null | no | The quality a non-functional requirement is about: `performance`, `scalability`, `availability`, `reliability`, `security`, `privacy`, `usability`, `accessibility`, `compatibility`, `maintainability`, `portability`, `compliance`, `observability`, `localisation` or `other`. Required for a non-functional requirement; ignored for a functional one. | | `statement` | string \| null | no | What the system shall do or keep, as HTML; filtered to a safe subset when read. | | `rationale` | string \| null | no | Why, as HTML. | | `source_stakeholder_id` | string \| null (uuid) | no | The id of the stakeholder it came from, a stakeholder of the same project; null for none. | | `source` | string \| null | no | Where it came from, in words, up to 300 characters. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority: `must`, `should`, `could` or `wont`. Defaults to `should`. | | `measure` | string \| null | no | The metric and target that say when a non-functional requirement is met, up to 500 characters; ignored for a functional one. | | `verification_method` | `test`, `inspection`, `analysis`, `demonstration` | no | How it is shown to be met: `test`, `inspection`, `analysis` or `demonstration`. Defaults to `test`. | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | no | Where it starts, on a create only: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. Defaults to `draft`. | | `owner_id` | string \| null (uuid) | no | The id of the person who answers for it, a member of the team; null for nobody. | ### Responses - **200**: The requirement. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The requirement's id. | | `data.project_id` | string | yes | The project the requirement belongs to. | | `data.type` | string | yes | The kind: `functional` or `non_functional`. Fixed once the requirement is numbered. | | `data.number` | integer | yes | Its number in the kind's own sequence. | | `data.reference` | string | yes | The key stories, test cases and people use, such as `CRM-FR-12` or `CRM-NFR-3`. | | `data.category` | string \| null | yes | The quality a non-functional requirement is about, such as `performance` or `security`; null for a functional one. | | `data.title` | string | yes | What the requirement is called. | | `data.statement` | string \| null | yes | What the system shall do or keep, as HTML filtered to a safe subset; null when none. | | `data.rationale` | string \| null | yes | Why, as HTML; null when none. | | `data.source_stakeholder_id` | string \| null | yes | The stakeholder it came from; null when none. | | `data.source_stakeholder_name` | string \| null | yes | That stakeholder's name, when the source was loaded; null otherwise. | | `data.source` | string \| null | yes | Where it came from, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.measure` | string \| null | yes | The metric and target that say when a non-functional requirement is met; null for a functional one or when not set. | | `data.verification_method` | string | yes | How it is shown to be met: `test`, `inspection`, `analysis` or `demonstration`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved`, `baselined` or `deprecated`. | | `data.owner_id` | string \| null | yes | The person who answers for it; null when unassigned. | | `data.owner_name` | string \| null | yes | That person's name, when the owner was loaded; null otherwise. | | `data.position` | integer | yes | Its place among the project's requirements, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the requirement was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the requirement last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `requirement:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when a non-functional requirement would be left without a category, when the source is not a stakeholder of the project, or when the owner is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Research API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/research **Research** is Heimdall reading the web for what a project's brief never states: what products of this kind commonly do, the laws, regulations and standards that apply in the client's country and industry, the integrations the client will expect, what the market already offers and the security and accessibility expectations the client will be held to. A **run** is one such reading, asked for by a person and worked on the queue as them; it writes **notes** (a topic, a summary and the sources it cites) under the run and leaves **suggestions** on the project, `research` for a fact the project must honour and `improvement` for a gap in its own record, which people keep or dismiss on the dashboard rail. A dismissed idea is remembered and never proposed again; a later run's notes sit beside the earlier ones. Tokens read and ask for research as staff with the `import:*` abilities: reading needs `import:view-any`, asking needs `import:create` and the project's AI assists on, and a client guest's token is refused everywhere here. Research spends the team's monthly AI budget, and one run is worked at a time per project. The suggestions are decided through the [suggestion endpoints](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports) of the brief import. ## Endpoints - `GET /v1/projects/{project}/research` — [List a project's research](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research) - `POST /v1/projects/{project}/research` — [Research a project's domain](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/research-a-projects-domain) - `GET /v1/projects/{project}/suggestions` — [List a project's research suggestions](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research-suggestions) ## Related - [Imports API](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports): The brief a run starts from, and the endpoints that keep or dismiss a suggestion. - [research.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research): The life of a research run: Heimdall asked to read a project's domain, done with notes and suggestions, or failed. --- # List a project's research suggestions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research-suggestions `GET /v1/projects/{project}/suggestions` ```bash curl https://api.example.test/v1/projects/$PROJECT_ID/suggestions \ -H "Authorization: Bearer $TOKEN" ``` The suggestions research left on the project, newest first, decided or not, in the same shape as a brief import's: each a `research` note on a fact the project must honour or an `improvement` on a gap in its own record, with its fields, sources, confidence, rationale and what was decided. Keep one with the accept endpoint, dismiss one with the reject endpoint. - Requires ability: `import:view-any` - MCP tools: `list_council_sessions`, `list_import_batches`, `list_research_notes` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | ### Responses - **200**: Array of `SuggestionResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The suggestion's id. | | `data[].project_id` | string | yes | The project the brief was imported into. | | `data[].batch_id` | string \| null | yes | The import batch it came from; null for a note from a research sweep. | | `data[].kind` | string | yes | What it proposes: `epic`, `story`, `functional_requirement`, `non_functional_requirement`, `business_rule`, `use_case`, `glossary_term`, `persona`, `stakeholder`, `risk`, `assumption`, `decision`, `improvement` or `research`. | | `data[].position` | integer | yes | Its place in the batch, from one, in the order Heimdall proposed. | | `data[].parent_id` | string \| null | yes | The suggestion it hangs under, such as a story's epic; null at the top. | | `data[].title` | string | yes | Its name, from the payload field its kind names: `title`, `name` or `term`. | | `data[].payload` | array \| null | yes | The fields of the record it would become, keyed as the record's own editor names them; the edited fields once accepted with edits. | | `data[].confidence` | number \| null | yes | How directly the brief supports it, 0 to 1; null when the model gave none. | | `data[].rationale` | string \| null | yes | One sentence on why Heimdall proposed it. | | `data[].source_quote` | string \| null | yes | The passage of the brief it drew on, word for word; null for an improvement. | | `data[].source_span` | array \| null | yes | Where that passage sits in the brief, `{ "start": …, "end": … }` as character offsets; null when the quote was not found. | | `data[].sources` | array \| null | yes | The sources a research note cites; null otherwise. | | `data[].voice` | string \| null | yes | The voice of the council that proposed it (`odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur` or `hel`); null for a brief import's or a research run's suggestion. | | `data[].status` | string | yes | What was decided: `pending`, `accepted`, `edited`, `merged`, `rejected` or `dismissed`. | | `data[].duplicate_type` | string \| null | yes | The alias of the record it may duplicate, or was merged into: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `glossary-term`, `persona`, `stakeholder`, `raid-item` or `decision`; null when none. | | `data[].duplicate_id` | string \| null | yes | That record's id; null when none. | | `data[].created_type` | string \| null | yes | The alias of the record the decision wrote; null until accepted, and for a note. | | `data[].created_id` | string \| null | yes | That record's id; null until accepted. | | `data[].decided_by` | string \| null | yes | The person who decided; null while pending or when the account is gone. | | `data[].decider_name` | string \| null | yes | That person's name, when the decider was loaded; null otherwise. | | `data[].decided_at` | string \| null | yes | When it was decided, ISO 8601; null while pending. | | `data[].created_at` | string \| null | yes | When Heimdall proposed it, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the project is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List a project's research Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research `GET /v1/projects/{project}/research` ```bash curl https://api.example.test/v1/projects/$PROJECT_ID/research \ -H "Authorization: Bearer $TOKEN" ``` Every run, newest first, each with where it stands, how many notes it wrote and suggestions it left, which provider and model answered, who asked, when, and the notes themselves: topic, summary as sanitised HTML and the sources cited. - Requires ability: `import:view-any` - MCP tools: `list_council_sessions`, `list_import_batches`, `list_research_notes` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | ### Responses - **200**: Array of `ResearchRunResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The run's id. | | `data[].project_id` | string | yes | The project researched. | | `data[].status` | string | yes | Where the run stands: `queued`, `running`, `ready` or `failed`. | | `data[].note_count` | integer | yes | How many notes it wrote; 0 until it is done. | | `data[].suggestion_count` | integer | yes | How many suggestions it left on the project, after the ideas already dismissed or held were left out. | | `data[].provider` | string \| null | yes | The provider that answered, by its configuration key; null until done. | | `data[].model` | string \| null | yes | The model that answered; null until done. | | `data[].usage_id` | string \| null | yes | The ledger row that filed the call's cost; null until done or when the row is gone. | | `data[].failure_reason` | string \| null | yes | Why the run failed, as a person reads it; null otherwise. | | `data[].created_by` | string \| null | yes | The person who asked; null when the account is gone. | | `data[].creator_name` | string \| null | yes | That person's name, when the creator was loaded; null otherwise. | | `data[].started_at` | string \| null | yes | When Heimdall began, ISO 8601; null while queued. | | `data[].finished_at` | string \| null | yes | When the run ended, well or badly, ISO 8601; null meanwhile. | | `data[].created_at` | string \| null | yes | When the run was asked for, ISO 8601. | | `data[].notes` | array of object | no | What Heimdall learned, in order; present when the notes were loaded. | | `data[].notes[].id` | string | yes | The note's id. | | `data[].notes[].run_id` | string | yes | The research run that wrote it. | | `data[].notes[].position` | integer | yes | Its place in the run, from one. | | `data[].notes[].topic` | string | yes | What the note is about. | | `data[].notes[].summary` | string | yes | What Heimdall learned, as sanitised HTML paragraphs. | | `data[].notes[].sources` | array of string | yes | The web addresses the note cites. | | `data[].notes[].created_at` | string \| null | yes | When the note was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `import:view-any`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the project is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Research a project's domain Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/research/research-a-projects-domain `POST /v1/projects/{project}/research` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/research \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Asks Heimdall to research the project: writes the run as queued, puts the work on the queue as the token's holder and emits `research.started`. Heimdall is told the project, its client's industry and country, the opening of the latest brief imported, the titles of the records the project holds and the ideas people already dismissed; it searches the web, writes up to eight notes under the run and leaves up to twelve suggestions on the project, leaving out any idea already dismissed or held. Poll the run until it is `ready` or `failed` (with the reason); one run is worked at a time per project. Answers `202` with the run, queued. - Requires ability: `import:create` - Fires events: `research.started` - MCP tools: `start_project_research` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **202**: The run, queued, `202`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The run's id. | | `data.project_id` | string | yes | The project researched. | | `data.status` | string | yes | Where the run stands: `queued`, `running`, `ready` or `failed`. | | `data.note_count` | integer | yes | How many notes it wrote; 0 until it is done. | | `data.suggestion_count` | integer | yes | How many suggestions it left on the project, after the ideas already dismissed or held were left out. | | `data.provider` | string \| null | yes | The provider that answered, by its configuration key; null until done. | | `data.model` | string \| null | yes | The model that answered; null until done. | | `data.usage_id` | string \| null | yes | The ledger row that filed the call's cost; null until done or when the row is gone. | | `data.failure_reason` | string \| null | yes | Why the run failed, as a person reads it; null otherwise. | | `data.created_by` | string \| null | yes | The person who asked; null when the account is gone. | | `data.creator_name` | string \| null | yes | That person's name, when the creator was loaded; null otherwise. | | `data.started_at` | string \| null | yes | When Heimdall began, ISO 8601; null while queued. | | `data.finished_at` | string \| null | yes | When the run ended, well or badly, ISO 8601; null meanwhile. | | `data.created_at` | string \| null | yes | When the run was asked for, ISO 8601. | | `data.notes` | array of object | no | What Heimdall learned, in order; present when the notes were loaded. | | `data.notes[].id` | string | yes | The note's id. | | `data.notes[].run_id` | string | yes | The research run that wrote it. | | `data.notes[].position` | integer | yes | Its place in the run, from one. | | `data.notes[].topic` | string | yes | What the note is about. | | `data.notes[].summary` | string | yes | What Heimdall learned, as sanitised HTML paragraphs. | | `data.notes[].sources` | array of string | yes | The web addresses the note cites. | | `data.notes[].created_at` | string \| null | yes | When the note was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, the holder's role on the project does not carry `import:create`, or the project's assists are off. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the project is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when a run is already queued or running for the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Create a role Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/create-a-role `POST /v1/roles` ```bash curl -X POST https://api.example.test/v1/roles \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "code": "support_agent", "name": "Support Agent", "description": "Answers tickets, cannot touch releases", "permissions": ["ticket:view-any", "ticket:view", "ticket:respond"] }' ``` > **Choose the code deliberately.** `code` is the stable identifier permissions are addressed by. It must be unique within the team and **cannot be changed afterwards**: renaming one would detach every assignment that referenced it. Only `name`, `description` and the permission set are mutable. `permissions` accepts codes from the permission catalogue in `entity:action` form. An unknown code is refused rather than silently dropped, so a typo fails loudly instead of creating a role that grants less than you think. Omit the field for a role that grants nothing yet. Answers `201` with the role and emits `role.created`. - Requires ability: `role:create` - Fires events: `role.created` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A role's team, code, name, description and permissions. On create `team_id`, `code` and `name` are required; on update `team_id` and `code` are prohibited (a role is neither moved nor renamed by code) and every other field is optional. | Field | Type | Required | Description | | --- | --- | --- | --- | | `team_id` | string (uuid) | yes | The team to create the role in; the caller must belong to it, and a team outside their account answers 404. Prohibited on update: a role cannot be moved between teams. | | `code` | string | yes | The stable identifier permissions are addressed by, up to 255 letters, numbers, dashes and underscores, unique within the team and immutable afterwards. Prohibited on update. | | `name` | string | yes | The display name, up to 255 characters. | | `description` | string \| null | no | What the role is for, up to 255 characters; null clears it. | | `permissions` | array of `client:view-any`, `client:view`, `client:create`, `client:update`, `client:delete`, `project:view-any`, `project:view`, `project:create`, `project:update`, `project:delete`, `project-member:view-any`, `project-member:view`, `project-member:create`, `project-member:update`, `project-member:delete`, `component:view-any`, `component:view`, `component:create`, `component:update`, `component:delete`, `environment:view-any`, `environment:view`, `environment:create`, `environment:update`, `environment:delete`, `environment:deploy`, `environment:freeze`, `repository:view-any`, `repository:view`, `repository:create`, `repository:update`, `repository:delete`, `stakeholder:view-any`, `stakeholder:view`, `stakeholder:create`, `stakeholder:update`, `stakeholder:delete`, `persona:view-any`, `persona:view`, `persona:create`, `persona:update`, `persona:delete`, `glossary:view-any`, `glossary:view`, `glossary:create`, `glossary:update`, `glossary:delete`, `data-entity:view-any`, `data-entity:view`, `data-entity:create`, `data-entity:update`, `data-entity:delete`, `design-reference:view-any`, `design-reference:view`, `design-reference:create`, `design-reference:update`, `design-reference:delete`, `epic:view-any`, `epic:view`, `epic:create`, `epic:update`, `epic:delete`, `epic:submit`, `epic:approve`, `work-item:view-any`, `work-item:view`, `work-item:create`, `work-item:update`, `work-item:delete`, `work-item:transition`, `work-item:assign`, `work-item:rank`, `requirement:view-any`, `requirement:view`, `requirement:create`, `requirement:update`, `requirement:delete`, `requirement:submit`, `requirement:approve`, `business-rule:view-any`, `business-rule:view`, `business-rule:create`, `business-rule:update`, `business-rule:delete`, `business-rule:submit`, `business-rule:approve`, `use-case:view-any`, `use-case:view`, `use-case:create`, `use-case:update`, `use-case:delete`, `raid:view-any`, `raid:view`, `raid:create`, `raid:update`, `raid:delete`, `decision:view-any`, `decision:view`, `decision:create`, `decision:update`, `decision:delete`, `link:view-any`, `link:view`, `link:create`, `link:update`, `link:delete`, `baseline:view-any`, `baseline:view`, `baseline:create`, `baseline:update`, `baseline:delete`, `baseline:freeze`, `approval:view-any`, `approval:view`, `approval:decide`, `approval:withdraw`, `approval:manage-policies`, `change-request:view-any`, `change-request:view`, `change-request:create`, `change-request:update`, `change-request:delete`, `change-request:analyse`, `change-request:approve`, `change-request:verify`, `sprint:view-any`, `sprint:view`, `sprint:create`, `sprint:update`, `sprint:delete`, `sprint:start`, `sprint:close`, `time-log:view-any`, `time-log:view`, `time-log:create`, `time-log:update`, `time-log:delete`, `time-log:approve`, `test-suite:view-any`, `test-suite:view`, `test-suite:create`, `test-suite:update`, `test-suite:delete`, `test-case:view-any`, `test-case:view`, `test-case:create`, `test-case:update`, `test-case:delete`, `test-case:approve`, `test-plan:view-any`, `test-plan:view`, `test-plan:create`, `test-plan:update`, `test-plan:delete`, `test-plan:approve`, `test-run:view-any`, `test-run:view`, `test-run:create`, `test-run:update`, `test-run:delete`, `test-run:execute`, `test-run:sign-off`, `release:view-any`, `release:view`, `release:create`, `release:update`, `release:delete`, `release:freeze`, `release:approve`, `release:publish`, `release:rollback`, `deployment:view-any`, `deployment:view`, `deployment:create`, `deployment:update`, `deployment:delete`, `deployment:record`, `deployment:promote`, `incident:view-any`, `incident:view`, `incident:create`, `incident:update`, `incident:delete`, `incident:command`, `incident:resolve`, `incident:publish-update`, `ticket:view-any`, `ticket:view`, `ticket:create`, `ticket:update`, `ticket:delete`, `ticket:respond`, `ticket:convert`, `maintenance-window:view-any`, `maintenance-window:view`, `maintenance-window:create`, `maintenance-window:update`, `maintenance-window:delete`, `knowledge:view-any`, `knowledge:view`, `knowledge:create`, `knowledge:update`, `knowledge:delete`, `document:view-any`, `document:view`, `document:create`, `document:update`, `document:delete`, `document:render`, `document:finalise`, `import:view-any`, `import:view`, `import:create`, `import:update`, `import:delete`, `comment:view-any`, `comment:view`, `comment:create`, `comment:update`, `comment:delete`, `comment:internal`, `attachment:view-any`, `attachment:view`, `attachment:create`, `attachment:update`, `attachment:delete`, `report:read`, `dashboard:read`, `activity:read`, `ai:use`, `ai:manage-budgets`, `team:view-any`, `team:view`, `team:create`, `team:update`, `team:delete`, `team-member:view-any`, `team-member:view`, `team-member:invite`, `team-member:remove`, `team-member:update-role`, `role:view-any`, `role:view`, `role:create`, `role:update`, `role:delete`, `group:view-any`, `group:view`, `group:create`, `group:update`, `group:delete`, `token:view-any`, `token:view`, `token:create`, `token:update`, `token:delete`, `webhook-endpoint:view-any`, `webhook-endpoint:view`, `webhook-endpoint:create`, `webhook-endpoint:update`, `webhook-endpoint:delete`, `webhook-delivery:view-any`, `webhook-delivery:view`, `webhook-delivery:retry`, `integration:manage` | no | The permission codes the role grants, in `entity:action` form. Sending it sets the role's permissions to exactly this list; omit the key to leave the existing set untouched. Omit it on create for a role that grants nothing yet. | ### Responses - **201**: The role as a 201. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The role's id. | | `data.team_id` | string | yes | The team the role belongs to. | | `data.code` | string | yes | The stable identifier permissions are addressed by, unique within the team and immutable after creation. | | `data.name` | string \| null | yes | The display name. | | `data.description` | string \| null | yes | What the role is for; null when none was given. | | `data.seeded` | boolean | yes | Whether this is one of the eight default roles every team starts with, which cannot be deleted. | | `data.permissions` | array of string | yes | The exact permission codes the role grants, the same strings used as token abilities, so roles can be matched against tokens without a second round-trip. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: when `team_id` is not one of the caller's teams. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a role with that code already exists in the team (`error.context.code` names it), or a permission code is unknown. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a role Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/delete-a-role `DELETE /v1/roles/{role}` ```bash curl -X DELETE https://api.example.test/v1/roles/$ROLE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Returns `204 No Content`, and detaches the role's permissions on the way out. Anyone currently holding the role loses whatever it granted them; they stay in the team. Emits `role.deleted`. Restricted to whoever **created** the role, unless the caller **owns** the team. The eight default roles cannot be deleted at all. - Requires ability: `role:delete` - Fires events: `role.deleted` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `role` | path | string (uuid) | yes | The role, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the role is one of the eight default roles, or when the caller neither created it nor owns the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a role Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/get-a-role `GET /v1/roles/{role}` One role with its permission codes. `permissions` is a flat array of the exact ability strings granted by this role, the same strings used as token abilities, so you can match roles against tokens without a second round trip. - Requires ability: `role:view` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `role` | path | string (uuid) | yes | The role, resolved by the route binder. | ### Responses - **200**: The role with its permissions. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The role's id. | | `data.team_id` | string | yes | The team the role belongs to. | | `data.code` | string | yes | The stable identifier permissions are addressed by, unique within the team and immutable after creation. | | `data.name` | string \| null | yes | The display name. | | `data.description` | string \| null | yes | What the role is for; null when none was given. | | `data.seeded` | boolean | yes | Whether this is one of the eight default roles every team starts with, which cannot be deleted. | | `data.permissions` | array of string | yes | The exact permission codes the role grants, the same strings used as token abilities, so roles can be matched against tokens without a second round-trip. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Roles API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/roles A **role** is the permission set a member holds on a team. Every team starts with eight: `admin`, `project_manager`, `business_analyst`, `developer`, `qa_engineer`, `release_manager`, `viewer` and `guest`, and an admin can add their own, a `support_agent` who may read and answer tickets but touch nothing else, a `finance` role that reads reports alone. Each member holds exactly one role, assigned when they are invited and changed through the [team members](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members) endpoints; [groups](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups) layer extra permissions on top of it for several people at once. On a project, a membership row can override the team role with a project role. A role's `permissions` is a flat array of the same `entity:action` strings a personal access token carries, so a role can be matched against a token without a second round trip, and the [ability catalogue](https://alm-docs.labs.zuselab.dev/api/v1/abilities) is the list to pick from. Changing a role changes what every holder may do from the next request; deleting one detaches its permissions and leaves its holders in the team with nothing beyond what their groups grant. Roles are read across every team the caller owns or belongs to, and a role on any other team is a `404 RESOURCE_NOT_FOUND`, never a `403`, so existence never leaks. Every write announces itself as a `role.*` event. ## Endpoints - `GET /v1/roles` — [List roles](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/list-roles) - `POST /v1/roles` — [Create a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/create-a-role) - `GET /v1/roles/{role}` — [Get a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/get-a-role) - `PATCH /v1/roles/{role}` — [Update a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/update-a-role) - `DELETE /v1/roles/{role}` — [Delete a role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/delete-a-role) ## Related - [Teams API](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams): A team is the tenant: the container every project, role, group, token and webhook endpoint hangs off. - [Team Members API](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members): Team members are the people inside a team: the owner and everyone invited to work in it, staff and client guests alike. - [Groups API](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups): Named permission bundles several people share - [Ability Catalogue](https://alm-docs.labs.zuselab.dev/api/v1/abilities): Every ability string a token can carry, how the catalogue mirrors the team permissions, what enforces it on the API today, and how to pick the right ones when minting. - [role.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role): Role creation, edits and deletion. --- # List roles Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/list-roles `GET /v1/roles` ```bash curl https://api.example.test/v1/roles \ -H "Authorization: Bearer $TOKEN" ``` Every role across the teams the caller can see, with its permission codes. Not paginated. `seeded` marks the eight default roles, which cannot be deleted. - Requires ability: `role:view-any` - MCP tools: `list_roles` ### Responses - **200**: Array of `RoleResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The role's id. | | `data[].team_id` | string | yes | The team the role belongs to. | | `data[].code` | string | yes | The stable identifier permissions are addressed by, unique within the team and immutable after creation. | | `data[].name` | string \| null | yes | The display name. | | `data[].description` | string \| null | yes | What the role is for; null when none was given. | | `data[].seeded` | boolean | yes | Whether this is one of the eight default roles every team starts with, which cannot be deleted. | | `data[].permissions` | array of string | yes | The exact permission codes the role grants, the same strings used as token abilities, so roles can be matched against tokens without a second round-trip. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a role Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/update-a-role `PATCH /v1/roles/{role}` ```bash curl -X PATCH https://api.example.test/v1/roles/$ROLE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"name": "Senior Support Agent", "permissions": ["ticket:view-any", "ticket:view", "ticket:respond", "ticket:convert"]}' ``` > **Permissions replace, they do not merge.** Sending `permissions` sets the role's permissions to exactly that list; anything omitted is revoked. **Omit the key entirely** to leave the existing set untouched while changing only the name or description. Answers `200` with the role and emits `role.updated`. - Requires ability: `role:update` - Fires events: `role.updated` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `role` | path | string (uuid) | yes | The role, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A role's team, code, name, description and permissions. On create `team_id`, `code` and `name` are required; on update `team_id` and `code` are prohibited (a role is neither moved nor renamed by code) and every other field is optional. | Field | Type | Required | Description | | --- | --- | --- | --- | | `team_id` | string (uuid) | yes | The team to create the role in; the caller must belong to it, and a team outside their account answers 404. Prohibited on update: a role cannot be moved between teams. | | `code` | string | yes | The stable identifier permissions are addressed by, up to 255 letters, numbers, dashes and underscores, unique within the team and immutable afterwards. Prohibited on update. | | `name` | string | yes | The display name, up to 255 characters. | | `description` | string \| null | no | What the role is for, up to 255 characters; null clears it. | | `permissions` | array of `client:view-any`, `client:view`, `client:create`, `client:update`, `client:delete`, `project:view-any`, `project:view`, `project:create`, `project:update`, `project:delete`, `project-member:view-any`, `project-member:view`, `project-member:create`, `project-member:update`, `project-member:delete`, `component:view-any`, `component:view`, `component:create`, `component:update`, `component:delete`, `environment:view-any`, `environment:view`, `environment:create`, `environment:update`, `environment:delete`, `environment:deploy`, `environment:freeze`, `repository:view-any`, `repository:view`, `repository:create`, `repository:update`, `repository:delete`, `stakeholder:view-any`, `stakeholder:view`, `stakeholder:create`, `stakeholder:update`, `stakeholder:delete`, `persona:view-any`, `persona:view`, `persona:create`, `persona:update`, `persona:delete`, `glossary:view-any`, `glossary:view`, `glossary:create`, `glossary:update`, `glossary:delete`, `data-entity:view-any`, `data-entity:view`, `data-entity:create`, `data-entity:update`, `data-entity:delete`, `design-reference:view-any`, `design-reference:view`, `design-reference:create`, `design-reference:update`, `design-reference:delete`, `epic:view-any`, `epic:view`, `epic:create`, `epic:update`, `epic:delete`, `epic:submit`, `epic:approve`, `work-item:view-any`, `work-item:view`, `work-item:create`, `work-item:update`, `work-item:delete`, `work-item:transition`, `work-item:assign`, `work-item:rank`, `requirement:view-any`, `requirement:view`, `requirement:create`, `requirement:update`, `requirement:delete`, `requirement:submit`, `requirement:approve`, `business-rule:view-any`, `business-rule:view`, `business-rule:create`, `business-rule:update`, `business-rule:delete`, `business-rule:submit`, `business-rule:approve`, `use-case:view-any`, `use-case:view`, `use-case:create`, `use-case:update`, `use-case:delete`, `raid:view-any`, `raid:view`, `raid:create`, `raid:update`, `raid:delete`, `decision:view-any`, `decision:view`, `decision:create`, `decision:update`, `decision:delete`, `link:view-any`, `link:view`, `link:create`, `link:update`, `link:delete`, `baseline:view-any`, `baseline:view`, `baseline:create`, `baseline:update`, `baseline:delete`, `baseline:freeze`, `approval:view-any`, `approval:view`, `approval:decide`, `approval:withdraw`, `approval:manage-policies`, `change-request:view-any`, `change-request:view`, `change-request:create`, `change-request:update`, `change-request:delete`, `change-request:analyse`, `change-request:approve`, `change-request:verify`, `sprint:view-any`, `sprint:view`, `sprint:create`, `sprint:update`, `sprint:delete`, `sprint:start`, `sprint:close`, `time-log:view-any`, `time-log:view`, `time-log:create`, `time-log:update`, `time-log:delete`, `time-log:approve`, `test-suite:view-any`, `test-suite:view`, `test-suite:create`, `test-suite:update`, `test-suite:delete`, `test-case:view-any`, `test-case:view`, `test-case:create`, `test-case:update`, `test-case:delete`, `test-case:approve`, `test-plan:view-any`, `test-plan:view`, `test-plan:create`, `test-plan:update`, `test-plan:delete`, `test-plan:approve`, `test-run:view-any`, `test-run:view`, `test-run:create`, `test-run:update`, `test-run:delete`, `test-run:execute`, `test-run:sign-off`, `release:view-any`, `release:view`, `release:create`, `release:update`, `release:delete`, `release:freeze`, `release:approve`, `release:publish`, `release:rollback`, `deployment:view-any`, `deployment:view`, `deployment:create`, `deployment:update`, `deployment:delete`, `deployment:record`, `deployment:promote`, `incident:view-any`, `incident:view`, `incident:create`, `incident:update`, `incident:delete`, `incident:command`, `incident:resolve`, `incident:publish-update`, `ticket:view-any`, `ticket:view`, `ticket:create`, `ticket:update`, `ticket:delete`, `ticket:respond`, `ticket:convert`, `maintenance-window:view-any`, `maintenance-window:view`, `maintenance-window:create`, `maintenance-window:update`, `maintenance-window:delete`, `knowledge:view-any`, `knowledge:view`, `knowledge:create`, `knowledge:update`, `knowledge:delete`, `document:view-any`, `document:view`, `document:create`, `document:update`, `document:delete`, `document:render`, `document:finalise`, `import:view-any`, `import:view`, `import:create`, `import:update`, `import:delete`, `comment:view-any`, `comment:view`, `comment:create`, `comment:update`, `comment:delete`, `comment:internal`, `attachment:view-any`, `attachment:view`, `attachment:create`, `attachment:update`, `attachment:delete`, `report:read`, `dashboard:read`, `activity:read`, `ai:use`, `ai:manage-budgets`, `team:view-any`, `team:view`, `team:create`, `team:update`, `team:delete`, `team-member:view-any`, `team-member:view`, `team-member:invite`, `team-member:remove`, `team-member:update-role`, `role:view-any`, `role:view`, `role:create`, `role:update`, `role:delete`, `group:view-any`, `group:view`, `group:create`, `group:update`, `group:delete`, `token:view-any`, `token:view`, `token:create`, `token:update`, `token:delete`, `webhook-endpoint:view-any`, `webhook-endpoint:view`, `webhook-endpoint:create`, `webhook-endpoint:update`, `webhook-endpoint:delete`, `webhook-delivery:view-any`, `webhook-delivery:view`, `webhook-delivery:retry`, `integration:manage` | no | The permission codes the role grants, in `entity:action` form. Sending it sets the role's permissions to exactly this list; omit the key to leave the existing set untouched. Omit it on create for a role that grants nothing yet. | ### Responses - **200**: The updated role. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The role's id. | | `data.team_id` | string | yes | The team the role belongs to. | | `data.code` | string | yes | The stable identifier permissions are addressed by, unique within the team and immutable after creation. | | `data.name` | string \| null | yes | The display name. | | `data.description` | string \| null | yes | What the role is for; null when none was given. | | `data.seeded` | boolean | yes | Whether this is one of the eight default roles every team starts with, which cannot be deleted. | | `data.permissions` | array of string | yes | The exact permission codes the role grants, the same strings used as token abilities, so roles can be matched against tokens without a second round-trip. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `code` or `team_id` is sent (a role cannot be renamed at the code level or moved between teams), or a permission code is unknown. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a stakeholder Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/add-a-stakeholder `POST /v1/projects/{project}/stakeholders` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/stakeholders \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Jane Perera", "email": "jane@acme.example", "organisation": "Acme Retail", "side": "client", "title": "Head of Digital", "role": "product_owner", "influence": "high", "interest": "high", "raci_default": "accountable", "approves_requirements": true, "signs_off_uat": true }' ``` Only `name` is required. The side defaults to `client`, the role to `other`, both ratings to `medium`, the three approval flags to false and `receives_status_reports` to true. An email, when given, must be unique among the project's stakeholders regardless of case. A `user_id`, when given, must be a member of the team and may link to one stakeholder per project. Answers `201` with the stakeholder and emits `stakeholder.created`. - Requires ability: `stakeholder:create` - Fires events: `stakeholder.created` - MCP tools: `create_stakeholder` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A stakeholder to add or change. Only `name` is required on a create; the side defaults to `client`, the role to `other` and the ratings to `medium`. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The stakeholder's name, 2 to 120 characters. | | `email` | string \| null (email) | no | Where to reach them; unique among the project's stakeholders regardless of case. | | `phone` | string \| null | no | A phone number, up to 40 characters. | | `organisation` | string \| null | no | The organisation they belong to, in words, up to 120 characters. | | `side` | `client`, `zuse`, `third_party` | no | Whose side they speak for: `client`, `zuse` or `third_party`. Defaults to `client` on a create. | | `title` | string \| null | no | Their job title, up to 120 characters. | | `role` | `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations`, `other` | no | What they do for the project: `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations` or `other`. Defaults to `other` on a create. | | `influence` | `low`, `medium`, `high` | no | How much weight their word carries: `low`, `medium` or `high`. Defaults to `medium` on a create. | | `interest` | `low`, `medium`, `high` | no | How closely they follow the project: `low`, `medium` or `high`. Defaults to `medium` on a create. | | `raci_default` | `responsible`, `accountable`, `consulted`, `informed` \| null | no | Their default letter in a RACI matrix: `responsible`, `accountable`, `consulted` or `informed`; null for none. | | `approves_requirements` | boolean | no | Whether they are asked to approve requirements. Defaults to false. | | `signs_off_uat` | boolean | no | Whether they sign off UAT runs. Defaults to false. | | `approves_releases` | boolean | no | Whether they approve releases. Defaults to false. | | `receives_status_reports` | boolean | no | Whether status reports go to them. Defaults to true. | | `notes` | string \| null | no | What the team should know, as HTML; filtered to a safe subset when read. | | `user_id` | string \| null (uuid) | no | The account they sign in with, a member of the team; null to unlink. One account links to one stakeholder per project. | ### Responses - **201**: The stakeholder, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The stakeholder's id. | | `data.project_id` | string | yes | The project the stakeholder belongs to. | | `data.user_id` | string \| null | no | The account the stakeholder signs in with, when linked; null otherwise. Left out for a guest. | | `data.name` | string | yes | The stakeholder's name. | | `data.email` | string \| null | no | Where to reach them; null when unknown. Left out for a guest. | | `data.phone` | string \| null | no | A phone number; null when unknown. Left out for a guest. | | `data.organisation` | string \| null | yes | The organisation they belong to, in words; null when unknown. | | `data.side` | string | yes | Whose side they speak for: `client`, `zuse` or `third_party`. | | `data.title` | string \| null | yes | Their job title; null when unknown. | | `data.role` | string | yes | What they do for the project: `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations` or `other`. | | `data.influence` | string | no | How much weight their word carries: `low`, `medium` or `high`. Left out for a guest. | | `data.interest` | string | no | How closely they follow the project: `low`, `medium` or `high`. Left out for a guest. | | `data.raci_default` | string \| null | no | Their default letter in a RACI matrix: `responsible`, `accountable`, `consulted`, `informed` or null. Left out for a guest. | | `data.approves_requirements` | boolean | yes | Whether they are asked to approve requirements. | | `data.signs_off_uat` | boolean | yes | Whether they sign off UAT runs. | | `data.approves_releases` | boolean | yes | Whether they approve releases. | | `data.receives_status_reports` | boolean | yes | Whether status reports go to them. | | `data.notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data.position` | integer | yes | Their place among the project's stakeholders, from zero. | | `data.created_at` | string \| null | yes | When the stakeholder was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the stakeholder last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `stakeholder:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when another stakeholder of the project carries the email, or when the account named is not a member of the team or is linked to another stakeholder of the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a stakeholder Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/get-a-stakeholder `GET /v1/stakeholders/{stakeholder}` ```bash curl https://api.example.test/v1/stakeholders/$STAKEHOLDER_ID \ -H "Authorization: Bearer $TOKEN" ``` One stakeholder. A stakeholder of a project the token may not see is `404 RESOURCE_NOT_FOUND`. - `side` is `client`, `zuse` or `third_party`; `role` runs `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations` and `other`. - `influence` and `interest` are `low`, `medium` or `high`; `raci_default` is `responsible`, `accountable`, `consulted`, `informed` or null. - `email`, `phone`, `influence`, `interest`, `raci_default`, `notes` and `user_id` are present for a staff token and absent for a guest's. - Requires ability: `stakeholder:view` - MCP tools: `get_stakeholder` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `stakeholder` | path | string (uuid) | yes | The stakeholder, bound by the middleware. | ### Responses - **200**: The stakeholder. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The stakeholder's id. | | `data.project_id` | string | yes | The project the stakeholder belongs to. | | `data.user_id` | string \| null | no | The account the stakeholder signs in with, when linked; null otherwise. Left out for a guest. | | `data.name` | string | yes | The stakeholder's name. | | `data.email` | string \| null | no | Where to reach them; null when unknown. Left out for a guest. | | `data.phone` | string \| null | no | A phone number; null when unknown. Left out for a guest. | | `data.organisation` | string \| null | yes | The organisation they belong to, in words; null when unknown. | | `data.side` | string | yes | Whose side they speak for: `client`, `zuse` or `third_party`. | | `data.title` | string \| null | yes | Their job title; null when unknown. | | `data.role` | string | yes | What they do for the project: `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations` or `other`. | | `data.influence` | string | no | How much weight their word carries: `low`, `medium` or `high`. Left out for a guest. | | `data.interest` | string | no | How closely they follow the project: `low`, `medium` or `high`. Left out for a guest. | | `data.raci_default` | string \| null | no | Their default letter in a RACI matrix: `responsible`, `accountable`, `consulted`, `informed` or null. Left out for a guest. | | `data.approves_requirements` | boolean | yes | Whether they are asked to approve requirements. | | `data.signs_off_uat` | boolean | yes | Whether they sign off UAT runs. | | `data.approves_releases` | boolean | yes | Whether they approve releases. | | `data.receives_status_reports` | boolean | yes | Whether status reports go to them. | | `data.notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data.position` | integer | yes | Their place among the project's stakeholders, from zero. | | `data.created_at` | string \| null | yes | When the stakeholder was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the stakeholder last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Stakeholders API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders A **stakeholder** is a person the project answers to: the client's sponsor and product owner, the people who sign off requirements, UAT runs and releases, Zuse's own account manager or architect, and third parties such as an auditor or a vendor. Each row carries the side they speak for, their role, how much weight their word carries and how closely they follow the project, their default letter in a RACI matrix, and four flags that say which approvals and reports name them. A stakeholder is a row on the record, not an account; when they sign in as a guest, `user_id` links the row to the account that decides. The reads here see the stakeholders of the projects the token may see. A client guest's token reads every stakeholder of its project without the contact details, the ratings, the notes and the linked account, which are left out of the payload rather than nulled. A stakeholder outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Stakeholders page runs, under the project permissions, and announces itself as a `stakeholder.*` event. ## Endpoints - `GET /v1/projects/{project}/stakeholders` — [List stakeholders](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-stakeholders) - `POST /v1/projects/{project}/stakeholders` — [Add a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/add-a-stakeholder) - `GET /v1/stakeholders/{stakeholder}` — [Get a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/get-a-stakeholder) - `PATCH /v1/stakeholders/{stakeholder}` — [Update a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/update-a-stakeholder) - `DELETE /v1/stakeholders/{stakeholder}` — [Remove a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/remove-a-stakeholder) - `GET /v1/stakeholders/{stakeholder}/revisions` — [List a stakeholder's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-a-stakeholders-versions) - `GET /v1/stakeholders/{stakeholder}/revisions/{revision}` — [Read one version of a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/read-one-version-of-a-stakeholder) - `POST /v1/stakeholders/{stakeholder}/revisions/{revision}/restore` — [Restore a version of a stakeholder](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/restore-a-version-of-a-stakeholder) ## Related - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects): The project a stakeholder belongs to. - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas): Who the project is built for, as opposed to who it answers to. - [Team Members API](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members): The accounts a stakeholder may be linked to. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): What changed on a stakeholder and who did it. - [stakeholder.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder): The lifecycle of a project's stakeholders: who was added to the record, what changed about them and who was removed. --- # List a stakeholder's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-a-stakeholders-versions `GET /v1/stakeholders/{stakeholder}/revisions` Every version the stakeholder was saved as, newest first, each with who wrote it and why, the columns it changed and the diff from the version before. - Requires ability: `stakeholder:view` - MCP tools: `get_stakeholder` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `stakeholder` | path | string (uuid) | yes | The stakeholder, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List stakeholders Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-stakeholders `GET /v1/projects/{project}/stakeholders` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/stakeholders?side=client&approves=requirements" \ -H "Authorization: Bearer $TOKEN" ``` The stakeholders of one project in the project's order, 25 per page (`per_page` 1 to 100). `sort_by` takes `name`, `side`, `role`, `influence`, `interest`, `position`, `created_at` or `updated_at`. `side`, `role`, `approves` (`requirements`, `uat` or `releases`: the people that approval names) and `search` narrow the page. A guest token receives each row without its internal fields. - Requires ability: `stakeholder:view-any` - MCP tools: `list_stakeholders` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `side` | query | `client`, `zuse`, `third_party` \| null | no | Narrow to one side: `client`, `zuse` or `third_party`. An unknown side is refused. | | `role` | query | `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations`, `other` \| null | no | Narrow to one role, such as `sponsor` or `product_owner`. An unknown role is refused. | | `approves` | query | `requirements`, `uat`, `releases`, null | no | Narrow to the people an approval names: `requirements`, `uat` or `releases`. | | `search` | query | string \| null | no | Words the name, the email, the organisation or the title must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of stakeholders. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The stakeholder's id. | | `data[].project_id` | string | yes | The project the stakeholder belongs to. | | `data[].user_id` | string \| null | no | The account the stakeholder signs in with, when linked; null otherwise. Left out for a guest. | | `data[].name` | string | yes | The stakeholder's name. | | `data[].email` | string \| null | no | Where to reach them; null when unknown. Left out for a guest. | | `data[].phone` | string \| null | no | A phone number; null when unknown. Left out for a guest. | | `data[].organisation` | string \| null | yes | The organisation they belong to, in words; null when unknown. | | `data[].side` | string | yes | Whose side they speak for: `client`, `zuse` or `third_party`. | | `data[].title` | string \| null | yes | Their job title; null when unknown. | | `data[].role` | string | yes | What they do for the project: `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations` or `other`. | | `data[].influence` | string | no | How much weight their word carries: `low`, `medium` or `high`. Left out for a guest. | | `data[].interest` | string | no | How closely they follow the project: `low`, `medium` or `high`. Left out for a guest. | | `data[].raci_default` | string \| null | no | Their default letter in a RACI matrix: `responsible`, `accountable`, `consulted`, `informed` or null. Left out for a guest. | | `data[].approves_requirements` | boolean | yes | Whether they are asked to approve requirements. | | `data[].signs_off_uat` | boolean | yes | Whether they sign off UAT runs. | | `data[].approves_releases` | boolean | yes | Whether they approve releases. | | `data[].receives_status_reports` | boolean | yes | Whether status reports go to them. | | `data[].notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data[].position` | integer | yes | Their place among the project's stakeholders, from zero. | | `data[].created_at` | string \| null | yes | When the stakeholder was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the stakeholder last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `side` or `role` is not one of its values, `approves` is not one of the three approvals, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Read one version of a stakeholder Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/read-one-version-of-a-stakeholder `GET /v1/stakeholders/{stakeholder}/revisions/{revision}` One version with its full snapshot, the diff from the version before, who wrote it and, for a restore, the version it put back. - Requires ability: `stakeholder:view` - MCP tools: `get_stakeholder` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `stakeholder` | path | string (uuid) | yes | The stakeholder, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version with its snapshot and diff. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Remove a stakeholder Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/remove-a-stakeholder `DELETE /v1/stakeholders/{stakeholder}` ```bash curl -X DELETE https://api.example.test/v1/stakeholders/$STAKEHOLDER_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a stakeholder from the project with their activity history. Returns `204 No Content` and emits `stakeholder.deleted`. Approvals and RACI overrides that will name stakeholders are not here yet; once they are, a stakeholder with decisions will be refused and kept for the record. - Requires ability: `stakeholder:delete` - Fires events: `stakeholder.deleted` - MCP tools: `delete_stakeholder` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `stakeholder` | path | string (uuid) | yes | The stakeholder, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `stakeholder:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a stakeholder Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/restore-a-version-of-a-stakeholder `POST /v1/stakeholders/{stakeholder}/revisions/{revision}/restore` Fills the version's snapshot back onto the stakeholder and writes a new version that points at it; nothing earlier is rewritten. - Requires ability: `stakeholder:update` - Fires events: `stakeholder.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `stakeholder` | path | string (uuid) | yes | The stakeholder, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version written, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a stakeholder Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/update-a-stakeholder `PATCH /v1/stakeholders/{stakeholder}` ```bash curl -X PATCH https://api.example.test/v1/stakeholders/$STAKEHOLDER_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"approves_releases": true, "raci_default": "accountable"}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. The same rules as adding apply to the email and the linked account. Answers `200` with the stakeholder and emits `stakeholder.updated` with the columns that changed, each with its old and new value; an update that changes nothing answers `200` and emits nothing. - Requires ability: `stakeholder:update` - Fires events: `stakeholder.updated` - MCP tools: `update_stakeholder` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `stakeholder` | path | string (uuid) | yes | The stakeholder, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A stakeholder to add or change. Only `name` is required on a create; the side defaults to `client`, the role to `other` and the ratings to `medium`. On an update, a field left out keeps its value and a field sent as null is cleared. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The stakeholder's name, 2 to 120 characters. | | `email` | string \| null (email) | no | Where to reach them; unique among the project's stakeholders regardless of case. | | `phone` | string \| null | no | A phone number, up to 40 characters. | | `organisation` | string \| null | no | The organisation they belong to, in words, up to 120 characters. | | `side` | `client`, `zuse`, `third_party` | no | Whose side they speak for: `client`, `zuse` or `third_party`. Defaults to `client` on a create. | | `title` | string \| null | no | Their job title, up to 120 characters. | | `role` | `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations`, `other` | no | What they do for the project: `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations` or `other`. Defaults to `other` on a create. | | `influence` | `low`, `medium`, `high` | no | How much weight their word carries: `low`, `medium` or `high`. Defaults to `medium` on a create. | | `interest` | `low`, `medium`, `high` | no | How closely they follow the project: `low`, `medium` or `high`. Defaults to `medium` on a create. | | `raci_default` | `responsible`, `accountable`, `consulted`, `informed` \| null | no | Their default letter in a RACI matrix: `responsible`, `accountable`, `consulted` or `informed`; null for none. | | `approves_requirements` | boolean | no | Whether they are asked to approve requirements. Defaults to false. | | `signs_off_uat` | boolean | no | Whether they sign off UAT runs. Defaults to false. | | `approves_releases` | boolean | no | Whether they approve releases. Defaults to false. | | `receives_status_reports` | boolean | no | Whether status reports go to them. Defaults to true. | | `notes` | string \| null | no | What the team should know, as HTML; filtered to a safe subset when read. | | `user_id` | string \| null (uuid) | no | The account they sign in with, a member of the team; null to unlink. One account links to one stakeholder per project. | ### Responses - **200**: The stakeholder. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The stakeholder's id. | | `data.project_id` | string | yes | The project the stakeholder belongs to. | | `data.user_id` | string \| null | no | The account the stakeholder signs in with, when linked; null otherwise. Left out for a guest. | | `data.name` | string | yes | The stakeholder's name. | | `data.email` | string \| null | no | Where to reach them; null when unknown. Left out for a guest. | | `data.phone` | string \| null | no | A phone number; null when unknown. Left out for a guest. | | `data.organisation` | string \| null | yes | The organisation they belong to, in words; null when unknown. | | `data.side` | string | yes | Whose side they speak for: `client`, `zuse` or `third_party`. | | `data.title` | string \| null | yes | Their job title; null when unknown. | | `data.role` | string | yes | What they do for the project: `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations` or `other`. | | `data.influence` | string | no | How much weight their word carries: `low`, `medium` or `high`. Left out for a guest. | | `data.interest` | string | no | How closely they follow the project: `low`, `medium` or `high`. Left out for a guest. | | `data.raci_default` | string \| null | no | Their default letter in a RACI matrix: `responsible`, `accountable`, `consulted`, `informed` or null. Left out for a guest. | | `data.approves_requirements` | boolean | yes | Whether they are asked to approve requirements. | | `data.signs_off_uat` | boolean | yes | Whether they sign off UAT runs. | | `data.approves_releases` | boolean | yes | Whether they approve releases. | | `data.receives_status_reports` | boolean | yes | Whether status reports go to them. | | `data.notes` | string \| null | no | What the team should know, as HTML filtered to a safe subset; null when none. Left out for a guest. | | `data.position` | integer | yes | Their place among the project's stakeholders, from zero. | | `data.created_at` | string \| null | yes | When the stakeholder was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the stakeholder last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `stakeholder:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when the new email is already carried by another stakeholder of the project, or when the account named is not a member of the team or is linked to another stakeholder of the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Change a team member's role Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/change-a-team-members-role `PATCH /v1/teams/{team}/members/{member}/role` ```bash curl -X PATCH https://api.example.test/v1/teams/$TEAM_ID/members/$USER_ID/role \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"role": "project_manager"}' ``` Moves a member onto another of the team's roles. The team **owner** cannot be re-roled: ownership is not a membership row and carries everything unconditionally. Answers `200` with the membership and emits `team.member_role_changed`. - Requires ability: `team-member:update-role` - Fires events: `team.member_role_changed` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | | `member` | path | string | yes | The membership, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The role to move a member onto. | Field | Type | Required | Description | | --- | --- | --- | --- | | `role` | string | yes | A role **code** on the team, such as `project_manager`, not a role id. An unknown code is refused. | ### Responses - **200**: The membership after the change. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The member's user id, the value the member routes take. | | `data.team_id` | string | yes | The team the membership is on. | | `data.name` | string | yes | The member's name. | | `data.email` | string | no | The member's email address. Present only for a token holding `team-member:view`; a roster-only token gets no `email` key at all, not null. The show route requires that ability, so it always includes it. | | `data.role_id` | string \| null | yes | The role the member holds on this team, or null for the owner pseudo-row. | | `data.role` | string \| null | yes | That role's code, such as `developer`; `admin` for the owner. | | `data.is_owner` | boolean | yes | True only for the team's owner, who is listed alongside the members but holds no membership row. | | `data.joined_at` | string \| null | yes | When the member joined, ISO 8601; the team's creation for the owner. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: for the owner, who has no role to change, or for someone who is not in the team. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the role code is not one of the team's. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a team member Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/get-a-team-member `GET /v1/teams/{team}/members/{member}` ```bash curl https://api.example.test/v1/teams/$TEAM_ID/members/$USER_ID \ -H "Authorization: Bearer $TOKEN" ``` One member, with their `email`, which this route always includes because it requires `team-member:view` by definition. `404 RESOURCE_NOT_FOUND` if the user is not on the team or the team is not in the caller's visibility set. - `role_id` points at a [role](/api/v1/reference/roles) on the same team, or `null` for the owner pseudo-row; `role` is that role's code, `admin` for the owner. - `is_owner` is `true` only for the team's creator, who is returned alongside the members but bypasses the membership table entirely. - Requires ability: `team-member:view` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | | `member` | path | string | yes | The person's place on the team, resolved by the route binder. | ### Responses - **200**: The membership. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The member's user id, the value the member routes take. | | `data.team_id` | string | yes | The team the membership is on. | | `data.name` | string | yes | The member's name. | | `data.email` | string | no | The member's email address. Present only for a token holding `team-member:view`; a roster-only token gets no `email` key at all, not null. The show route requires that ability, so it always includes it. | | `data.role_id` | string \| null | yes | The role the member holds on this team, or null for the owner pseudo-row. | | `data.role` | string \| null | yes | That role's code, such as `developer`; `admin` for the owner. | | `data.is_owner` | boolean | yes | True only for the team's owner, who is listed alongside the members but holds no membership row. | | `data.joined_at` | string \| null | yes | When the member joined, ISO 8601; the team's creation for the owner. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Team Members API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members **Team members** are the people inside a team: the owner and everyone invited to work in it, staff and client guests alike. A member joins by invitation to an email address with a role code, holds one [role](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles) at a time, and leaves by removal; a pending invitation is withdrawn rather than removed, with the same ability, because both revoke the same prospective access. The team's owner appears as the first row of the roster but is not a membership: ownership cannot be re-roled or removed here. A **guest** is a member too: a client stakeholder invited with the `guest` role and one project, who sees that project alone and nothing of the team. Inviting a guest therefore names the project, and accepting the invitation writes the project membership beside the team one. Reading who is on a team and reading how to reach them are separate asks, so `email` is present only when the token carries `team-member:view` and is otherwise absent from the row altogether. The lists are not paginated, since a team's roster is small by construction. Every change announces itself as a `team.*` event. ## What the API refuses | Attempt | Result | | ---------------------------------------------- | ---------------------------------------- | | Re-role or remove the **owner** | `404`: ownership is not a membership row | | Invite someone already in the team or already invited | `422` | | Name a `role` code the team does not have | `422` | | Invite a `guest` without a `project_id`, or name one for any other role | `422` | | Address a user who is not in the team | `404` | | Address a team the caller cannot see | `404` | A team is visible only when the caller owns it or is a member. Tokens scoped to a different team get `404 RESOURCE_NOT_FOUND` on every route here; existence never leaks. ## Endpoints - `GET /v1/teams/{team}/invitations` — [List a team's pending invitations](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/list-a-teams-pending-invitations) - `GET /v1/teams/{team}/members` — [List a team's members](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/list-a-teams-members) - `POST /v1/teams/{team}/members` — [Invite a team member](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/invite-a-team-member) - `GET /v1/teams/{team}/members/{member}` — [Get a team member](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/get-a-team-member) - `DELETE /v1/teams/{team}/members/{member}` — [Remove a team member](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/remove-a-team-member) - `PATCH /v1/teams/{team}/members/{member}/role` — [Change a team member's role](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/change-a-team-members-role) - `DELETE /v1/teams/{team}/invitations/{invitation}` — [Withdraw an invitation](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/withdraw-an-invitation) ## Related - [Roles API](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles): A role is the permission set a member holds on a team. - [Teams API](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams): A team is the tenant: the container every project, role, group, token and webhook endpoint hangs off. - [Ability Catalogue](https://alm-docs.labs.zuselab.dev/api/v1/abilities): Every ability string a token can carry, how the catalogue mirrors the team permissions, what enforces it on the API today, and how to pick the right ones when minting. - [team.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team): Team creation, rename and deletion, and the comings and goings of the people in it. --- # Invite a team member Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/invite-a-team-member `POST /v1/teams/{team}/members` ```bash curl -X POST https://api.example.test/v1/teams/$TEAM_ID/members \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"email": "priya@example.com", "role": "business_analyst", "name": "Priya Patel"}' ``` Addressed by **email**, not user id: the invitee may not have an account yet, which is what separates inviting from adding. They receive an invitation message with a link; the membership row appears when they accept. `role` is a role **code** on that team, such as `developer`, not a role id. To invite a client guest, send `"role": "guest"` with the `project_id` they are invited to: ```bash curl -X POST https://api.example.test/v1/teams/$TEAM_ID/members \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"email": "client@example.com", "role": "guest", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Anil Perera", "title": "Product Owner"}' ``` Answers `201` with the invitation. Emits `team.member_invited`, then `team.member_joined` when they accept, and `project.member_added` as well for a guest. - Requires ability: `team-member:invite` - Fires events: `team.member_invited` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) Who to invite and as what. An invitation names an email rather than a user, because the invitee may not have an account yet; a guest also names the project they are invited to. | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | The invitee's email address, up to 255 characters. They receive an invitation message with a link; the membership appears when they accept. | | `role` | string | yes | A role **code** on that team, such as `developer` or `guest`, not a role id. An unknown code is refused. | | `project_id` | string \| null (uuid) | no | For the `guest` role, the one project the person is invited to; refused for any other role, and required for a guest. | | `name` | string \| null | no | The invitee's name, up to 255 characters, shown on the invitation and used for the account it creates. | | `title` | string \| null | no | The invitee's job title, up to 255 characters; optional. | ### Responses - **201**: 201 with the invitation. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The invitation's id, the value the withdraw route takes. | | `data.team_id` | string | yes | The team the person was invited into. | | `data.email` | string | yes | The address the invitation was sent to. | | `data.role_id` | string | yes | The role the person will hold on acceptance. | | `data.project_id` | string \| null | yes | For a guest, the one project they are invited onto; null for staff. | | `data.name` | string \| null | yes | The invitee's name as the inviter typed it; null when not given. | | `data.title` | string \| null | yes | The invitee's job title as the inviter typed it; null when not given. | | `data.invited_by` | string \| null | yes | The user who sent the invitation; null when that account is gone. | | `data.created_at` | string \| null | yes | When the invitation was sent, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the role code is not one of the team's, the invitee is already a member or already invited, a guest is invited without a project, the project is not the team's, or a project is named for a role other than `guest`. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List a team's members Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/list-a-teams-members `GET /v1/teams/{team}/members` ```bash curl https://api.example.test/v1/teams/$TEAM_ID/members \ -H "Authorization: Bearer $TOKEN" ``` The team's owner as the first row followed by every member, guests included. Not paginated. `email` is **omitted entirely** unless the token carries `team-member:view`. A roster-only token gets every other field and no `email` key at all: not `null`, absent. - Requires ability: `team-member:view-any` - MCP tools: `list_team_members` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | ### Responses - **200**: Array of `TeamMemberResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The member's user id, the value the member routes take. | | `data[].team_id` | string | yes | The team the membership is on. | | `data[].name` | string | yes | The member's name. | | `data[].email` | string | no | The member's email address. Present only for a token holding `team-member:view`; a roster-only token gets no `email` key at all, not null. The show route requires that ability, so it always includes it. | | `data[].role_id` | string \| null | yes | The role the member holds on this team, or null for the owner pseudo-row. | | `data[].role` | string \| null | yes | That role's code, such as `developer`; `admin` for the owner. | | `data[].is_owner` | boolean | yes | True only for the team's owner, who is listed alongside the members but holds no membership row. | | `data[].joined_at` | string \| null | yes | When the member joined, ISO 8601; the team's creation for the owner. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List a team's pending invitations Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/list-a-teams-pending-invitations `GET /v1/teams/{team}/invitations` ```bash curl https://api.example.test/v1/teams/$TEAM_ID/invitations \ -H "Authorization: Bearer $TOKEN" ``` Every invitation not yet accepted, newest first. Not paginated. An invitation that was accepted is gone from here and present on the roster; one that was withdrawn is gone from both. - Requires ability: `team-member:view-any` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | ### Responses - **200**: Array of `TeamInvitationResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The invitation's id, the value the withdraw route takes. | | `data[].team_id` | string | yes | The team the person was invited into. | | `data[].email` | string | yes | The address the invitation was sent to. | | `data[].role_id` | string | yes | The role the person will hold on acceptance. | | `data[].project_id` | string \| null | yes | For a guest, the one project they are invited onto; null for staff. | | `data[].name` | string \| null | yes | The invitee's name as the inviter typed it; null when not given. | | `data[].title` | string \| null | yes | The invitee's job title as the inviter typed it; null when not given. | | `data[].invited_by` | string \| null | yes | The user who sent the invitation; null when that account is gone. | | `data[].created_at` | string \| null | yes | When the invitation was sent, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Remove a team member Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/remove-a-team-member `DELETE /v1/teams/{team}/members/{member}` ```bash curl -X DELETE https://api.example.test/v1/teams/$TEAM_ID/members/$USER_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Returns `204 No Content`. They lose access to every project and setting scoped to the team immediately, their project membership rows go with them, and their own account is untouched. Emits `team.member_removed`. - Requires ability: `team-member:remove` - Fires events: `team.member_removed` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | | `member` | path | string | yes | The membership, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: for the owner, who cannot be removed. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Withdraw an invitation Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/withdraw-an-invitation `DELETE /v1/teams/{team}/invitations/{invitation}` Requires `team-member:remove`, the same ability as removing a member, because withdrawing an invitation and removing a member revoke the same prospective access. ```bash curl -X DELETE https://api.example.test/v1/teams/$TEAM_ID/invitations/$INVITATION_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` > **An invitee is not a member.** Someone who has not accepted has no membership row to remove, only a pending invitation. That is why this is a separate route: removing a member and withdrawing an invitation fail on different things, and using the wrong one returns `404` rather than doing something surprising. Returns `204 No Content` and the invitation link stops working. Nothing is emitted. - Requires ability: `team-member:remove` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | | `invitation` | path | string | yes | The pending invitation, resolved within the team by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Create a team Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/create-a-team `POST /v1/teams` ```bash curl -X POST https://api.example.test/v1/teams \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"name": "Client Success"}' ``` The caller becomes the team **owner**, and the eight default roles (`admin`, `project_manager`, `business_analyst`, `developer`, `qa_engineer`, `release_manager`, `viewer`, `guest`) are seeded at once. A new team starts with nobody else in it: add people with [Team Members](/api/v1/reference/team-members). Answers `201` with the team and emits `team.created` and one `role.created` per seeded role. - Requires ability: `team:create` - Fires events: `team.created` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A team's name: required on create, optional on update. Ownership does not transfer through this body, and membership moves through its own routes. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The team's name, up to 255 characters; the only mutable field. | ### Responses - **201**: The new team. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The team's id, the value a token's team scope names. | | `data.name` | string | yes | The team's name. | | `data.owner_user_id` | string | yes | The user who owns the team; renaming and deleting it are theirs alone. | | `data.created_at` | string \| null | yes | When the team was created, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a team Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/delete-a-team `DELETE /v1/teams/{team}` ```bash curl -X DELETE https://api.example.test/v1/teams/$TEAM_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Returns `204 No Content` and emits `team.deleted`. The team's roles, groups, memberships, tokens' scopes and webhook endpoints go with it. > **A team that holds projects cannot be deleted.** Projects carry a client's record and are never removed as a side effect; move or delete them first, and the refusal names how many there are. An owner also cannot delete their only team, because an account with no tenant is a state nothing else in the product can produce. - Requires ability: `team:delete` - Fires events: `team.deleted` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the caller is a member rather than the owner, when the team still holds projects, or when it is the caller's only team; `error.message` says which. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a team Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-a-team `GET /v1/teams/{team}` ```bash curl https://api.example.test/v1/teams/$TEAM_ID \ -H "Authorization: Bearer $TOKEN" ``` The team if the caller belongs to it; `404 RESOURCE_NOT_FOUND` otherwise, never `403`, so a foreign team's existence does not leak. - Requires ability: `team:view` - MCP tools: `get_team` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | ### Responses - **200**: The team. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The team's id, the value a token's team scope names. | | `data.name` | string | yes | The team's name. | | `data.owner_user_id` | string | yes | The user who owns the team; renaming and deleting it are theirs alone. | | `data.created_at` | string \| null | yes | When the team was created, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get the current team Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-the-current-team `GET /v1/teams/current` ```bash curl https://api.example.test/v1/teams/current \ -H "Authorization: Bearer $TOKEN" ``` Exactly the team the token is scoped to through `scope:team:`. A connection test calls this on save to validate a bearer token: a `200` proves the token is live and names the tenant it acts in. - Requires ability: `team:view` - MCP tools: `get_me` ### Responses - **200**: The team named by the token's `scope:team:` ability. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The team's id, the value a token's team scope names. | | `data.name` | string | yes | The team's name. | | `data.owner_user_id` | string | yes | The user who owns the team; renaming and deleting it are theirs alone. | | `data.created_at` | string \| null | yes | When the team was created, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `TENANT_MISMATCH`: with `reason: missing_team_scope` when the token carries no team scope. `RESOURCE_NOT_FOUND`: when the scoped team no longer exists. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Teams API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/teams A **team** is the tenant: the container every project, role, group, token and webhook endpoint hangs off. Zuse Technologies itself is a team, and more can be created for a department or a client account. Every token names one team through the `scope:team:` ability it was minted with, and these endpoints let you read and manage the teams your account can act on. The owner is the person who created the team. Ownership is not a membership row: the owner cannot be re-roled or removed, and renaming or deleting the team is theirs alone. Every write announces itself as a `team.*` event. ## Endpoints - `GET /v1/teams` — [List teams](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/list-teams) - `POST /v1/teams` — [Create a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/create-a-team) - `GET /v1/teams/current` — [Get the current team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-the-current-team) - `GET /v1/teams/{team}` — [Get a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-a-team) - `PATCH /v1/teams/{team}` — [Rename a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/rename-a-team) - `DELETE /v1/teams/{team}` — [Delete a team](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/delete-a-team) ## Related - [Team Members API](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members): Invite, re-role and remove people. - [Roles API](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles): The permission sets a member can hold. - [Groups API](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups): Named permission bundles several people share. - [team.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team): Team creation, rename and deletion, and the comings and goings of the people in it. --- # List teams Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/list-teams `GET /v1/teams` ```bash curl https://api.example.test/v1/teams \ -H "Authorization: Bearer $TOKEN" ``` Every team the caller owns or is a member of; other people's teams are invisible. Never paginated. Useful for a client that wants to show a team picker before a token scoped to one team is minted. - Requires ability: `team:view-any` - MCP tools: `list_teams` ### Responses - **200**: Array of `TeamResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The team's id, the value a token's team scope names. | | `data[].name` | string | yes | The team's name. | | `data[].owner_user_id` | string | yes | The user who owns the team; renaming and deleting it are theirs alone. | | `data[].created_at` | string \| null | yes | When the team was created, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Rename a team Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/rename-a-team `PATCH /v1/teams/{team}` ```bash curl -X PATCH https://api.example.test/v1/teams/$TEAM_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"name": "Client Success (EU)"}' ``` `name` is the only mutable field. Ownership does not transfer through this endpoint, and membership moves through its own routes. Owner only. Answers `200` with the team and emits `team.updated`. - Requires ability: `team:update` - Fires events: `team.updated` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `team` | path | string (uuid) | yes | The team, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A team's name: required on create, optional on update. Ownership does not transfer through this body, and membership moves through its own routes. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The team's name, up to 255 characters; the only mutable field. | ### Responses - **200**: The team after the change. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The team's id, the value a token's team scope names. | | `data.name` | string | yes | The team's name. | | `data.owner_user_id` | string | yes | The user who owns the team; renaming and deleting it are theirs alone. | | `data.created_at` | string \| null | yes | When the team was created, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the caller belongs to the team but does not own it. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a token Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/get-a-token `GET /v1/tokens/{token}` ```bash curl https://api.example.test/v1/tokens/$TOKEN_ID \ -H "Authorization: Bearer $TOKEN" ``` One of the caller's tokens, without its value. - `id` is an **integer** serialised as a string, the one identifier in the platform that is not a UUID. See [Identifiers](/api/v1#identifiers). - `abilities` is the gate strings only. Scope tuples (`scope:team:…`, `scope:project:…`) are split out into `scopes` for readability, so `abilities_count` counts real permissions rather than bookkeeping. - The plaintext value is **never** returned here. Lose it and the only recovery is to revoke the token and mint another. - Requires ability: `token:view` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `token` | path | integer | yes | One of the caller's tokens, resolved by the route binder. | ### Responses - **200**: The token. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The token's id: an **integer** serialised as a string, the one identifier in the platform that is not a UUID. | | `data.name` | string | yes | The label the token was minted with. | | `data.abilities` | array of any | yes | The gate abilities only; the scope tuples are split out into `scopes`. | | `data.scopes` | object | yes | The token's confinement, read out of its `scope:team:` and `scope:project:` abilities as structure rather than opaque strings. | | `data.scopes.team_id` | string | yes | The team the token is scoped to; null for a token minted without a team scope. | | `data.scopes.project_ids` | array of string | yes | The projects the token is confined to; empty when it reaches every project of the team. | | `data.abilities_count` | integer | yes | How many gate abilities the token holds, scopes excluded. | | `data.last_used_at` | string \| null | yes | When the token last authenticated a request, ISO 8601; null until it has. | | `data.expires_at` | string \| null | yes | When the token stops working, ISO 8601; null for a token that does not expire. | | `data.created_at` | string \| null | yes | When the token was minted, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Tokens API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens Personal access tokens are what authorise every REST and MCP call. They look like `zg_live__` in production and `zg_test__` everywhere else, and the plaintext value exists exactly once: in the response that created it. You can mint one in the app under **Account Settings → API Tokens**, or through the mint endpoint below. Minting and revoking announce themselves as `token.created` and `token.revoked`, behind the `token:view` ability, so a security mirror can watch credential churn on the team. ## Authorization Ownership is necessary but not sufficient: each route demands its matching `token:*` ability as well, because a token scoped to nothing but `dashboard:read` must not be able to enumerate and revoke every other token its owner holds. Tokens belonging to another person are invisible: every route returns `404 RESOURCE_NOT_FOUND` for an id outside the caller's own set, so the API cannot be used to probe which ids exist. ## Endpoints - `GET /v1/tokens` — [List the caller's tokens](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/list-the-callers-tokens) - `POST /v1/tokens` — [Mint a token](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/mint-a-token) - `GET /v1/tokens/{token}` — [Get a token](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/get-a-token) - `DELETE /v1/tokens/{token}` — [Revoke a token](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/revoke-a-token) ## Related - [Authentication](https://alm-docs.labs.zuselab.dev/api/v1/authentication): The scope:team: tuple and the bearer header. - [Ability Catalogue](https://alm-docs.labs.zuselab.dev/api/v1/abilities): Every ability string a token can carry. - [Tenancy & Scopes](https://alm-docs.labs.zuselab.dev/api/v1/tenancy-and-scopes): How team and project scoping combine, and why a guest's token is always project-scoped. - [token.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token): Personal access token minting and revocation. --- # List the caller's tokens Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/list-the-callers-tokens `GET /v1/tokens` ```bash curl https://api.example.test/v1/tokens \ -H "Authorization: Bearer $TOKEN" ``` Paginated, newest first. `per_page` accepts 1 to 100 and defaults to 25. - Requires ability: `token:view-any` - MCP tools: `list_tokens` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | ### Responses - **200**: The caller's tokens, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The token's id: an **integer** serialised as a string, the one identifier in the platform that is not a UUID. | | `data[].name` | string | yes | The label the token was minted with. | | `data[].abilities` | array of any | yes | The gate abilities only; the scope tuples are split out into `scopes`. | | `data[].scopes` | object | yes | The token's confinement, read out of its `scope:team:` and `scope:project:` abilities as structure rather than opaque strings. | | `data[].scopes.team_id` | string | yes | The team the token is scoped to; null for a token minted without a team scope. | | `data[].scopes.project_ids` | array of string | yes | The projects the token is confined to; empty when it reaches every project of the team. | | `data[].abilities_count` | integer | yes | How many gate abilities the token holds, scopes excluded. | | `data[].last_used_at` | string \| null | yes | When the token last authenticated a request, ISO 8601; null until it has. | | `data[].expires_at` | string \| null | yes | When the token stops working, ISO 8601; null for a token that does not expire. | | `data[].created_at` | string \| null | yes | When the token was minted, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Mint a token Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/mint-a-token `POST /v1/tokens` ```bash curl -X POST https://api.example.test/v1/tokens \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Nightly report", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "abilities": ["project:view-any", "project:view", "activity:read"] }' ``` `token` sits alongside `data`, not inside it, the same shape webhook endpoints use for their one-time signing secret. Store it before you do anything else; there is no endpoint that will hand it back. Minting is deliberately not offered as an MCP tool. **`team_id` is required here, and only here.** Everywhere else in v1 the team comes from the calling token's frozen scope and a `team_id` in the body is ignored. Minting is the exception: a token is created **for** a team, and you may belong to several. The minted token gets `scope:team:` automatically and cannot be widened later. Send `project_id` as well to confine it to one project of that team; a guest's token must name one of their projects. > **A token can never mint a stronger token.** The abilities you request must be a **subset** of the abilities the calling token holds, and `token:create` is withheld unconditionally from anything minted this way. A compromised credential can therefore produce nothing its holder could not already do directly, and no ladder of tokens can descend from one leak with no single revocation that ends the chain. A person in the app is not attenuated: they authenticated with a password and a second factor, not with a bearer string, so they can grant anything in the catalogue. Asking for more than you hold returns `422`, and `error.message` names the abilities that were refused: ```json { "error": { "code": "VALIDATION_FAILED", "message": "A token cannot grant abilities it does not hold: project:delete, release:approve." } } ``` - Requires ability: `token:create` - Fires events: `token.created` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A token to mint: its label, the abilities it holds, the team it is minted for and, optionally, the one project it is confined to. The abilities must be a subset of the calling token's, and `token:create` is never granted this way. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | A label for the token, up to 255 characters, shown on Account Settings → API Tokens. | | `abilities` | array of string | yes | The abilities the token holds, at least one. Each must be one the calling token holds itself (attenuation), and `token:create` is withheld unconditionally. | | `team_id` | string (uuid) | yes | The team the token is minted **for**; it receives `scope:team:` automatically and cannot be widened later. Must be a team the caller owns or belongs to. | | `project_id` | string \| null (uuid) | no | Confine the token to one project of that team; it receives `scope:project:`. A guest's token must name one of their projects. | ### Responses - **201**: The token plus the plaintext, as a 201. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The token's id: an **integer** serialised as a string, the one identifier in the platform that is not a UUID. | | `data.name` | string | yes | The label the token was minted with. | | `data.abilities` | array of any | yes | The gate abilities only; the scope tuples are split out into `scopes`. | | `data.scopes` | object | yes | The token's confinement, read out of its `scope:team:` and `scope:project:` abilities as structure rather than opaque strings. | | `data.scopes.team_id` | string | yes | The team the token is scoped to; null for a token minted without a team scope. | | `data.scopes.project_ids` | array of string | yes | The projects the token is confined to; empty when it reaches every project of the team. | | `data.abilities_count` | integer | yes | How many gate abilities the token holds, scopes excluded. | | `data.last_used_at` | string \| null | yes | When the token last authenticated a request, ISO 8601; null until it has. | | `data.expires_at` | string \| null | yes | When the token stops working, ISO 8601; null for a token that does not expire. | | `data.created_at` | string \| null | yes | When the token was minted, ISO 8601. | | `token` | string | yes | The plaintext token, `zg___`, returned exactly once. Store it before anything else. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a requested ability is not held by the calling token, `token:create` is requested, `team_id` is not a team the caller owns or belongs to, `project_id` is not a project they can see, a guest omits the project, an ability is retired or unknown, or `abilities` is empty. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Revoke a token Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/revoke-a-token `DELETE /v1/tokens/{token}` ```bash curl -X DELETE https://api.example.test/v1/tokens/$TOKEN_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Returns `204 No Content` and emits `token.revoked`. Revocation is immediate: in-flight requests already authenticated will finish, and the next one fails. - Requires ability: `token:delete` - Fires events: `token.revoked` - MCP tools: `revoke_token` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `token` | path | integer | yes | One of the caller's tokens, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when no token with that id belongs to the authenticated person. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when you tried to revoke the exact token you are authenticating with. Revoke it from the app, or from another token on the same account. This exists so an automation cannot lock itself out mid-run and leave the failure looking like a network fault. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a use case Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/add-a-use-case `POST /v1/projects/{project}/use-cases` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/use-cases \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "title": "Redeem a voucher at checkout", "actor_persona_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "goal": "Pay less for the basket with a voucher the member holds", "trigger": "The member opens the checkout with a voucher in their wallet", "priority": "must", "steps": [ { "actor_action": "The member opens the checkout", "system_response": "The system shows the basket and the vouchers in the wallet" }, { "actor_action": "The member picks a voucher", "system_response": "The system applies the discount and shows the new total" }, { "flow": "exception", "flow_label": "2a", "branches_from": 2, "actor_action": "The member picks a voucher that expired", "system_response": "The system refuses it, says why and keeps the total" } ] }' ``` Only `title` is required. The use case takes the next number of the use cases' sequence and the reference spelled from the project's key, starts as a `draft` unless `status` says otherwise and `should` unless `priority` does. The actor must be a persona of the project. A step of an alternate or exception flow must say in `branches_from` which main step it leaves, and that step must exist in the main flow. The save writes version one of the use case, steps included. Answers `201` with the use case and its steps and emits `use_case.created`. - Requires ability: `use-case:create` - Fires events: `use_case.created` - MCP tools: `create_use_case` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A use case to add or change. Only `title` is required on a create; the priority defaults to `should` and the status to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; `steps`, when sent, replaces the list; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the use case is called, 3 to 200 characters. | | `actor_persona_id` | string \| null (uuid) | no | The id of the persona who acts, a persona of the same project; null for none. | | `secondary_actors` | array \| null | no | The other actors in the room, up to ten names of at most 120 characters each. | | `goal` | string \| null | no | What the actor is trying to achieve, up to 300 characters. | | `trigger` | string \| null | no | What starts the use case, up to 300 characters. | | `preconditions` | string \| null | no | What must be true before, as HTML; filtered to a safe subset when read. | | `postconditions` | string \| null | no | What is true after, as HTML. | | `frequency` | string \| null | no | How often it happens, in words up to 120 characters. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority: `must`, `should`, `could` or `wont`. Defaults to `should`. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where it starts, on a create only: `draft`, `in_review`, `approved` or `deprecated`. Defaults to `draft`. | | `steps` | array \| null | no | The steps to replace the use case's with, up to sixty rows of `actor_action` (required), `system_response`, `flow`, `flow_label`, `branches_from` and an `id` to keep an existing row. Left out, the steps stay as they are. | | `steps[].id` | string \| null (uuid) | no | The id of an existing step to keep and change; leave it out to add a row. | | `steps[].flow` | `main`, `alternate`, `exception` \| null | no | The flow the step belongs to: `main`, `alternate` or `exception`. Defaults to `main`. | | `steps[].flow_label` | string \| null | no | The label a branch carries, such as `3a`, up to 20 characters; ignored for a main step. | | `steps[].actor_action` | string | yes | What the actor does, 3 to 500 characters; required. | | `steps[].system_response` | string \| null | no | What the system does in reply, up to 500 characters. | | `steps[].branches_from` | integer \| null | no | The main step an alternate or exception step leaves, numbered from one; required for a branch, ignored for a main step. | ### Responses - **201**: The use case, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The use case's id. | | `data.project_id` | string | yes | The project the use case belongs to. | | `data.number` | integer | yes | Its number in the use cases' own sequence. | | `data.reference` | string | yes | The key stories, test cases and people use, such as `CRM-UC-4`. | | `data.title` | string | yes | What the use case is called. | | `data.actor_persona_id` | string \| null | yes | The persona who acts; null when none is named. | | `data.actor_persona_name` | string \| null | yes | That persona's name, when the actor was loaded; null otherwise. | | `data.secondary_actors` | array of string | yes | The other actors in the room, in words; empty when none. | | `data.goal` | string \| null | yes | What the actor is trying to achieve; null when none. | | `data.trigger` | string \| null | yes | What starts the use case; null when none. | | `data.preconditions` | string \| null | yes | What must be true before, as HTML filtered to a safe subset; null when none. | | `data.postconditions` | string \| null | yes | What is true after, as HTML; null when none. | | `data.frequency` | string \| null | yes | How often it happens, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved` or `deprecated`. | | `data.steps` | array of any | yes | The steps in order, each a use case step, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's use cases, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the use case was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the use case last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `use-case:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when a step lacks its action, when a branch names no main step or one the flow does not have, or when the actor is not a persona of the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a use case Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/delete-a-use-case `DELETE /v1/use-cases/{useCase}` ```bash curl -X DELETE https://api.example.test/v1/use-cases/$USE_CASE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes a use case with its steps, its versions and its activity history. The number it took is never handed out again. Returns `204 No Content` and emits `use_case.deleted`. Prefer a move to `deprecated` for a use case the client withdrew; it keeps the record. - Requires ability: `use-case:delete` - Fires events: `use_case.deleted` - MCP tools: `delete_use_case` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `useCase` | path | string (uuid) | yes | The use case, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `use-case:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a use case Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-use-case `GET /v1/use-cases/{useCase}` ```bash curl https://api.example.test/v1/use-cases/$USE_CASE_ID \ -H "Authorization: Bearer $TOKEN" ``` One use case with its actor persona's name and its steps in order. A use case of a project the token may not see, or a draft read through a client guest's token, is `404 RESOURCE_NOT_FOUND`. - `status` is `draft`, `in_review`, `approved` or `deprecated`; `priority` is `must`, `should`, `could` or `wont`; a step's `flow` is `main`, `alternate` or `exception`, and a branch carries a `flow_label` and the main step it leaves in `branches_from`. - Requires ability: `use-case:view` - MCP tools: `get_use_case` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `useCase` | path | string (uuid) | yes | The use case, bound by the middleware. | ### Responses - **200**: The use case with its actor and its steps. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The use case's id. | | `data.project_id` | string | yes | The project the use case belongs to. | | `data.number` | integer | yes | Its number in the use cases' own sequence. | | `data.reference` | string | yes | The key stories, test cases and people use, such as `CRM-UC-4`. | | `data.title` | string | yes | What the use case is called. | | `data.actor_persona_id` | string \| null | yes | The persona who acts; null when none is named. | | `data.actor_persona_name` | string \| null | yes | That persona's name, when the actor was loaded; null otherwise. | | `data.secondary_actors` | array of string | yes | The other actors in the room, in words; empty when none. | | `data.goal` | string \| null | yes | What the actor is trying to achieve; null when none. | | `data.trigger` | string \| null | yes | What starts the use case; null when none. | | `data.preconditions` | string \| null | yes | What must be true before, as HTML filtered to a safe subset; null when none. | | `data.postconditions` | string \| null | yes | What is true after, as HTML; null when none. | | `data.frequency` | string \| null | yes | How often it happens, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved` or `deprecated`. | | `data.steps` | array of any | yes | The steps in order, each a use case step, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's use cases, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the use case was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the use case last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a version of a use case Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-version-of-a-use-case `GET /v1/use-cases/{useCase}/revisions/{revision}` ```bash curl https://api.example.test/v1/use-cases/$USE_CASE_ID/revisions/$REVISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One version of the use case. A version of another artefact, a version the token may not see or a client guest's token is `404 RESOURCE_NOT_FOUND`. - Requires ability: `use-case:view` - MCP tools: `get_use_case` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `useCase` | path | string (uuid) | yes | The use case, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Use Cases API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases A **use case** is how an actor reaches a goal with the system, step by step: the persona who acts, the other actors in the room, the goal, what triggers it, what must be true before and after, how often it happens, and the **steps** (what the actor does and what the system does in reply) in a main flow with alternate and exception flows branching off numbered main steps. Use cases number on their own sequence (`CRM-UC-4`), carry a MoSCoW priority and a status from draft through review to approved or deprecated, and the stories that cover a flow will be drafted from its steps once the inline assists land. Every save of a use case writes a **version** that also carries its steps, so a use case and its flows are restored together. The versions are read and put back through the endpoints below, under the use case's own abilities. The reads here see the use cases of the projects the token may see; a client guest's token sees the approved ones only, because a draft is the analyst's working note until the client signs it off. A use case outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Use Cases page runs, under the project permissions, and announces itself as a `use_case.*` event. ## Endpoints - `GET /v1/projects/{project}/use-cases` — [List use cases](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-use-cases) - `POST /v1/projects/{project}/use-cases` — [Add a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/add-a-use-case) - `GET /v1/use-cases/{useCase}` — [Get a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-use-case) - `PATCH /v1/use-cases/{useCase}` — [Update a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/update-a-use-case) - `DELETE /v1/use-cases/{useCase}` — [Delete a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/delete-a-use-case) - `GET /v1/use-cases/{useCase}/revisions` — [List a use case's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-a-use-cases-versions) - `GET /v1/use-cases/{useCase}/revisions/{revision}` — [Get a version of a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-version-of-a-use-case) - `POST /v1/use-cases/{useCase}/status` — [Move a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/move-a-use-case) - `PUT /v1/use-cases/{useCase}/steps` — [Replace a use case's steps](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/replace-a-use-cases-steps) - `POST /v1/use-cases/{useCase}/revisions/{revision}/restore` — [Restore a version of a use case](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/restore-a-version-of-a-use-case) ## Related - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas): The people a use case is acted by. - [Requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements): And [work items](/api/v1/reference/work-items): what a use case specifies and the stories that cover its flows, once links land. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): Who changed a use case and when, beside the versions themselves. - [use_case.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case): The lifecycle of a project's use cases: a use case added, changed, moved along its review lifecycle, given new steps, put back to an earlier version and deleted. --- # List a use case's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-a-use-cases-versions `GET /v1/use-cases/{useCase}/revisions` ```bash curl "https://api.example.test/v1/use-cases/$USE_CASE_ID/revisions" \ -H "Authorization: Bearer $TOKEN" ``` Every version of the use case, newest first, 25 per page (`per_page` 1 to 100). Each carries its `version` number, the `reason` it was written, who wrote it (`author_id`, `author_name`) and through what (`actor_kind`), a one-line `summary`, the `changed_fields`, the `diff` from the version before and the full `snapshot`, whose `steps` key holds the step rows as they were. A client guest's token is refused. - Requires ability: `use-case:view` - MCP tools: `get_use_case` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `useCase` | path | string (uuid) | yes | The use case, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role does not carry `use-case:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List use cases Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-use-cases `GET /v1/projects/{project}/use-cases` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/use-cases?status=approved" \ -H "Authorization: Bearer $TOKEN" ``` The use cases of one project in the project's order, 25 per page (`per_page` 1 to 100), each with its actor persona's name and without its steps, which the single read carries. `sort_by` takes `number`, `reference`, `title`, `status`, `priority`, `frequency`, `position`, `created_at` or `updated_at`. `status`, `priority` and `persona_id` narrow the page to one value each, and `search` to the use cases whose reference, title or goal contain the words. - Requires ability: `use-case:view-any` - MCP tools: `list_use_cases` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `status` | query | `draft`, `in_review`, `approved`, `deprecated` \| null | no | Narrow to one status: `draft`, `in_review`, `approved` or `deprecated`. An unknown status is refused. | | `priority` | query | `must`, `should`, `could`, `wont` \| null | no | Narrow to one MoSCoW priority: `must`, `should`, `could` or `wont`. | | `persona_id` | query | string \| null (uuid) | no | Narrow to the use cases one persona acts in. | | `search` | query | string \| null | no | Words the reference, the title or the goal must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of use cases with their actors; the steps are read one use case at a time. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The use case's id. | | `data[].project_id` | string | yes | The project the use case belongs to. | | `data[].number` | integer | yes | Its number in the use cases' own sequence. | | `data[].reference` | string | yes | The key stories, test cases and people use, such as `CRM-UC-4`. | | `data[].title` | string | yes | What the use case is called. | | `data[].actor_persona_id` | string \| null | yes | The persona who acts; null when none is named. | | `data[].actor_persona_name` | string \| null | yes | That persona's name, when the actor was loaded; null otherwise. | | `data[].secondary_actors` | array of string | yes | The other actors in the room, in words; empty when none. | | `data[].goal` | string \| null | yes | What the actor is trying to achieve; null when none. | | `data[].trigger` | string \| null | yes | What starts the use case; null when none. | | `data[].preconditions` | string \| null | yes | What must be true before, as HTML filtered to a safe subset; null when none. | | `data[].postconditions` | string \| null | yes | What is true after, as HTML; null when none. | | `data[].frequency` | string \| null | yes | How often it happens, in words; null when none. | | `data[].priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data[].status` | string | yes | Where it stands: `draft`, `in_review`, `approved` or `deprecated`. | | `data[].steps` | array of any | yes | The steps in order, each a use case step, when they were loaded; empty otherwise. | | `data[].position` | integer | yes | Its place among the project's use cases, from zero. | | `data[].version` | integer | yes | How many times it was approved; zero until the first approval. | | `data[].approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data[].approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data[].created_at` | string \| null | yes | When the use case was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the use case last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` or `priority` is not one of its values, `persona_id` is not a uuid, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a use case Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/move-a-use-case `POST /v1/use-cases/{useCase}/status` ```bash curl -X POST https://api.example.test/v1/use-cases/$USE_CASE_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "in_review"}' ``` Moves the use case along its lifecycle: a deprecated use case goes back to `draft` only, and every other state may move to any other. A move into `approved` stamps when the use case was agreed, and a client guest reads it while it is approved. The move writes a version with the reason `transition`. Answers `200` with the use case and emits `use_case.status_changed` with the status before and after; a move to the status the use case is in already answers `200` and emits nothing. - Requires ability: `use-case:update` - Fires events: `use_case.status_changed` - MCP tools: `update_use_case` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `useCase` | path | string (uuid) | yes | The use case, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the use case to. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the use case goes: `draft`, `in_review`, `approved` or `deprecated`. A deprecated use case goes back to draft only; every other move is allowed. | ### Responses - **200**: The use case, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The use case's id. | | `data.project_id` | string | yes | The project the use case belongs to. | | `data.number` | integer | yes | Its number in the use cases' own sequence. | | `data.reference` | string | yes | The key stories, test cases and people use, such as `CRM-UC-4`. | | `data.title` | string | yes | What the use case is called. | | `data.actor_persona_id` | string \| null | yes | The persona who acts; null when none is named. | | `data.actor_persona_name` | string \| null | yes | That persona's name, when the actor was loaded; null otherwise. | | `data.secondary_actors` | array of string | yes | The other actors in the room, in words; empty when none. | | `data.goal` | string \| null | yes | What the actor is trying to achieve; null when none. | | `data.trigger` | string \| null | yes | What starts the use case; null when none. | | `data.preconditions` | string \| null | yes | What must be true before, as HTML filtered to a safe subset; null when none. | | `data.postconditions` | string \| null | yes | What is true after, as HTML; null when none. | | `data.frequency` | string \| null | yes | How often it happens, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved` or `deprecated`. | | `data.steps` | array of any | yes | The steps in order, each a use case step, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's use cases, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the use case was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the use case last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `use-case:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, or when the lifecycle refuses the move. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Replace a use case's steps Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/replace-a-use-cases-steps `PUT /v1/use-cases/{useCase}/steps` ```bash curl -X PUT https://api.example.test/v1/use-cases/$USE_CASE_ID/steps \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "steps": [ { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "actor_action": "The member opens the checkout", "system_response": "The system shows the basket and the vouchers in the wallet" }, { "actor_action": "The member picks a voucher and confirms", "system_response": "The system applies the discount, shows the new total and marks the voucher used" } ] }' ``` Sends the full list in order: a row with an `id` keeps that row and changes it, a row without one is new, and every row left out is removed; an empty list removes every step. The replace writes a version of the use case. Answers `200` with the use case and its steps and emits `use_case.steps_changed` with how many rows were added, changed and removed; a replace that changes nothing answers `200` and emits nothing. - Requires ability: `use-case:update` - Fires events: `use_case.steps_changed` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `useCase` | path | string (uuid) | yes | The use case, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The full list of steps the use case should have, in order. A row with an `id` keeps that row; a row without one is new; rows left out are removed. | Field | Type | Required | Description | | --- | --- | --- | --- | | `steps` | array of object | yes | Up to sixty rows of `actor_action` (required, 3 to 500 characters), `system_response` (up to 500 characters), `flow` (`main`, `alternate` or `exception`, `main` by default), `flow_label` (up to 20 characters, for a branch), `branches_from` (the main step a branch leaves, numbered from one) and an `id` to keep an existing row. An empty list removes every step. | | `steps[].id` | string \| null (uuid) | no | The id of an existing step to keep and change; leave it out to add a row. | | `steps[].flow` | `main`, `alternate`, `exception` \| null | no | The flow the step belongs to: `main`, `alternate` or `exception`. Defaults to `main`. | | `steps[].flow_label` | string \| null | no | The label a branch carries, such as `3a`, up to 20 characters; ignored for a main step. | | `steps[].actor_action` | string | yes | What the actor does, 3 to 500 characters; required. | | `steps[].system_response` | string \| null | no | What the system does in reply, up to 500 characters. | | `steps[].branches_from` | integer \| null | no | The main step an alternate or exception step leaves, numbered from one; required for a branch, ignored for a main step. | ### Responses - **200**: The use case with its steps. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The use case's id. | | `data.project_id` | string | yes | The project the use case belongs to. | | `data.number` | integer | yes | Its number in the use cases' own sequence. | | `data.reference` | string | yes | The key stories, test cases and people use, such as `CRM-UC-4`. | | `data.title` | string | yes | What the use case is called. | | `data.actor_persona_id` | string \| null | yes | The persona who acts; null when none is named. | | `data.actor_persona_name` | string \| null | yes | That persona's name, when the actor was loaded; null otherwise. | | `data.secondary_actors` | array of string | yes | The other actors in the room, in words; empty when none. | | `data.goal` | string \| null | yes | What the actor is trying to achieve; null when none. | | `data.trigger` | string \| null | yes | What starts the use case; null when none. | | `data.preconditions` | string \| null | yes | What must be true before, as HTML filtered to a safe subset; null when none. | | `data.postconditions` | string \| null | yes | What is true after, as HTML; null when none. | | `data.frequency` | string \| null | yes | How often it happens, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved` or `deprecated`. | | `data.steps` | array of any | yes | The steps in order, each a use case step, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's use cases, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the use case was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the use case last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `use-case:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a row lacks its action, a branch names no main step or one the flow does not have, a flow is unknown or the list is longer than sixty. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a use case Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/restore-a-version-of-a-use-case `POST /v1/use-cases/{useCase}/revisions/{revision}/restore` ```bash curl -X POST https://api.example.test/v1/use-cases/$USE_CASE_ID/revisions/$REVISION_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the version's captured columns and its steps back onto the use case, status included, and saves. The save writes a new version with the reason `restore` that names the one put back in `restored_from_id`; nothing earlier is rewritten. Answers `201` with the version written and emits `use_case.restored` with both version numbers. - Requires ability: `use-case:update` - Fires events: `use_case.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `useCase` | path | string (uuid) | yes | The use case, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version the restore wrote, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `use-case:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the version is not the use case's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the version is identical to the current record, so there is nothing to restore. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a use case Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/update-a-use-case `PATCH /v1/use-cases/{useCase}` ```bash curl -X PATCH https://api.example.test/v1/use-cases/$USE_CASE_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"priority": "must", "frequency": "A few hundred times a day in a campaign week", "trigger": null}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. `steps`, when sent, replaces the list in full with the fields in one save; left out, the steps stay as they are. The status does not change here; the move endpoint owns it. A save that changes a captured column or the steps writes a new version. Answers `200` with the use case and its steps and emits `use_case.updated` with the columns that changed, each with its old and new value; an update that changes no column answers `200` and emits nothing. - Requires ability: `use-case:update` - Fires events: `use_case.updated` - MCP tools: `update_use_case` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `useCase` | path | string (uuid) | yes | The use case, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A use case to add or change. Only `title` is required on a create; the priority defaults to `should` and the status to `draft`. On an update, a field left out keeps its value and a field sent as null is cleared; `steps`, when sent, replaces the list; the status moves through its own endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | What the use case is called, 3 to 200 characters. | | `actor_persona_id` | string \| null (uuid) | no | The id of the persona who acts, a persona of the same project; null for none. | | `secondary_actors` | array \| null | no | The other actors in the room, up to ten names of at most 120 characters each. | | `goal` | string \| null | no | What the actor is trying to achieve, up to 300 characters. | | `trigger` | string \| null | no | What starts the use case, up to 300 characters. | | `preconditions` | string \| null | no | What must be true before, as HTML; filtered to a safe subset when read. | | `postconditions` | string \| null | no | What is true after, as HTML. | | `frequency` | string \| null | no | How often it happens, in words up to 120 characters. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority: `must`, `should`, `could` or `wont`. Defaults to `should`. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where it starts, on a create only: `draft`, `in_review`, `approved` or `deprecated`. Defaults to `draft`. | | `steps` | array \| null | no | The steps to replace the use case's with, up to sixty rows of `actor_action` (required), `system_response`, `flow`, `flow_label`, `branches_from` and an `id` to keep an existing row. Left out, the steps stay as they are. | | `steps[].id` | string \| null (uuid) | no | The id of an existing step to keep and change; leave it out to add a row. | | `steps[].flow` | `main`, `alternate`, `exception` \| null | no | The flow the step belongs to: `main`, `alternate` or `exception`. Defaults to `main`. | | `steps[].flow_label` | string \| null | no | The label a branch carries, such as `3a`, up to 20 characters; ignored for a main step. | | `steps[].actor_action` | string | yes | What the actor does, 3 to 500 characters; required. | | `steps[].system_response` | string \| null | no | What the system does in reply, up to 500 characters. | | `steps[].branches_from` | integer \| null | no | The main step an alternate or exception step leaves, numbered from one; required for a branch, ignored for a main step. | ### Responses - **200**: The use case. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The use case's id. | | `data.project_id` | string | yes | The project the use case belongs to. | | `data.number` | integer | yes | Its number in the use cases' own sequence. | | `data.reference` | string | yes | The key stories, test cases and people use, such as `CRM-UC-4`. | | `data.title` | string | yes | What the use case is called. | | `data.actor_persona_id` | string \| null | yes | The persona who acts; null when none is named. | | `data.actor_persona_name` | string \| null | yes | That persona's name, when the actor was loaded; null otherwise. | | `data.secondary_actors` | array of string | yes | The other actors in the room, in words; empty when none. | | `data.goal` | string \| null | yes | What the actor is trying to achieve; null when none. | | `data.trigger` | string \| null | yes | What starts the use case; null when none. | | `data.preconditions` | string \| null | yes | What must be true before, as HTML filtered to a safe subset; null when none. | | `data.postconditions` | string \| null | yes | What is true after, as HTML; null when none. | | `data.frequency` | string \| null | yes | How often it happens, in words; null when none. | | `data.priority` | string | yes | Its MoSCoW priority: `must`, `should`, `could` or `wont`. | | `data.status` | string | yes | Where it stands: `draft`, `in_review`, `approved` or `deprecated`. | | `data.steps` | array of any | yes | The steps in order, each a use case step, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's use cases, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.approved_revision_id` | string \| null | yes | The revision the last approval froze; null until the approvals land. | | `data.created_at` | string \| null | yes | When the use case was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the use case last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `use-case:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when a branch is dangling, or when the actor is not a persona of the project. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Follow an artefact Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/follow-an-artefact `POST /v1/projects/{project}/watchers` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/watchers \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "artefact_type": "work-item", "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" }' ``` Follows an artefact of the project as the token's holder. Following twice is one subscription, answered with the same row. Answers `201` with the subscription. - Requires ability: `comment:view` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) An artefact to follow as the token's holder. Following twice is one subscription. | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `artefact_id` | string (uuid) | yes | The artefact's id, one of the project's. | ### Responses - **201**: The subscription, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The subscription's id. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.artefact_type` | string | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data.artefact_id` | string | yes | The artefact's id. | | `data.user_id` | string | yes | The person following it. | | `data.user_name` | string \| null | yes | The person's name, when the person was loaded; null otherwise. | | `data.created_at` | string \| null | yes | When they started following, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the artefact is hidden from the token, missing or of another project. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `artefact_type` is not one of the nine or `artefact_id` is not a uuid. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Watchers API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers A **watcher** is a person following one artefact: they are told of every comment posted on it. A person follows by hand from the artefact's comments panel or through this API, and follows without asking when they comment. Following is personal: a token follows and stops following for its holder only, and the subscription changes nothing about the artefact. A token reads the followers of the artefacts its holder may see. Following an artefact needs only that the holder may see it; there is no permission of its own, and the routes are gated by the comment abilities because following is about comment notifications. ## Endpoints - `GET /v1/projects/{project}/watchers` — [List an artefact's followers](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/list-an-artefacts-followers) - `POST /v1/projects/{project}/watchers` — [Follow an artefact](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/follow-an-artefact) - `DELETE /v1/watchers/{watcher}` — [Stop following](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/stop-following) ## Related - [Comments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments): What the followers are told of. - [Notifications](https://alm-docs.labs.zuselab.dev/notifications): Where the alerts go. --- # List an artefact's followers Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/list-an-artefacts-followers `GET /v1/projects/{project}/watchers` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/watchers?artefact_type=work-item&artefact_id=$WORK_ITEM_ID" \ -H "Authorization: Bearer $TOKEN" ``` Everyone following one artefact, earliest first, each with their name. `artefact_type` and `artefact_id` are required; the artefact must be one the token may see. - Requires ability: `comment:view-any` - MCP tools: `list_comments` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project. | | `artefact_type` | query | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. Required. | | `artefact_id` | query | string (uuid) | yes | The artefact's id. Required. | ### Responses - **200**: Array of `WatcherResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The subscription's id. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].artefact_type` | string | yes | The artefact's type: `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`. | | `data[].artefact_id` | string | yes | The artefact's id. | | `data[].user_id` | string | yes | The person following it. | | `data[].user_name` | string \| null | yes | The person's name, when the person was loaded; null otherwise. | | `data[].created_at` | string \| null | yes | When they started following, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the artefact is hidden from the token, missing or of another project. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `artefact_type` is not one of the nine or `artefact_id` is not a uuid. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Stop following Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/stop-following `DELETE /v1/watchers/{watcher}` ```bash curl -X DELETE https://api.example.test/v1/watchers/$WATCHER_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes the token holder's own subscription. Returns `204 No Content`. - Requires ability: `comment:view` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `watcher` | path | string (uuid) | yes | The subscription. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the subscription is someone else's. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the subscription is of a project the token may not see, on an artefact hidden from it, or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a webhook delivery Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/get-a-webhook-delivery `GET /v1/webhook-deliveries/{delivery}` ```bash curl https://api.example.test/v1/webhook-deliveries/$DELIVERY_ID \ -H "Authorization: Bearer $TOKEN" ``` One delivery. Another team's delivery is a `404 RESOURCE_NOT_FOUND`, never a `403`. A row is born `pending`, becomes `failed` after a non-2xx or transport error while retries remain, `delivered` on the first 2xx, and `dead` once the ladder is exhausted or the endpoint vanished. `delivered` and `dead` are final until a retry resets the row to `pending`. There is no `updated_at`: a delivery only moves forward through its statuses, and the three timestamps say when each transition happened. > **`payload` discloses nothing new.** The envelope is what your own endpoint already received, or would have had it answered. Reading it needs `webhook-delivery:view`, and the tenant scope reaches a delivery only through an endpoint of the token's team. - Requires ability: `webhook-delivery:view` - MCP tools: `get_webhook_delivery` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `delivery` | path | string (uuid) | yes | The delivery, resolved by the route binder. | ### Responses - **200**: The delivery with the envelope its endpoint received. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The delivery's id, the value the retry endpoint takes. | | `data.endpoint_id` | string | yes | The webhook endpoint the event was posted to. | | `data.event` | string | yes | The event name, as the catalogue lists it. | | `data.event_id` | string | yes | The 26-character ULID the envelope's `id` carries without its `evt_` prefix; also the `ZG-Event-Id` header the handler received. | | `data.status` | string | yes | `pending` (born, or reset by a retry), `failed` (a non-2xx or transport error while retries remain), `delivered` (the first 2xx) or `dead` (the ladder ran out or the endpoint vanished). `delivered` and `dead` are final until a retry. | | `data.attempts` | integer | yes | Posts made so far. Reset to 0 by a retry. | | `data.response_status` | integer \| null | yes | The HTTP status the endpoint returned on the last attempt; null before the first post or after a transport failure. | | `data.response_excerpt` | string \| null | yes | The start of the response body, or the transport error; null before the first post. | | `data.payload` | array of any | yes | The full event envelope that was, or would have been, posted, so a consumer can be debugged against it exactly. | | `data.next_attempt_at` | string \| null | yes | When the worker will post a `failed` row again, ISO 8601; null otherwise. | | `data.delivered_at` | string \| null | yes | Stamped on the first 2xx, ISO 8601; null until then. | | `data.dead_lettered_at` | string \| null | yes | Stamped when the ladder ran out, ISO 8601; null otherwise. | | `data.created_at` | string | yes | When the row was born, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Webhook Deliveries API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries Every event Zuse Bifrost posts to one of your [webhook endpoints](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) is a **delivery**: one row per endpoint per event, carrying the signed envelope that was sent, the target's response and where the row is on the [retry ladder](https://alm-docs.labs.zuselab.dev/webhooks/v1/retries-and-delivery). This is the same log the app shows under **Settings → Webhook Deliveries**, over REST, so an integrator on call can see why a consumer rejected an event without signing in. The two replay paths are the app's too. A single failed or dead-lettered row can be retried from the start of the ladder, and everything the team dead-lettered since an instant can be replayed at once after a fix. ## Endpoints - `GET /v1/webhook-deliveries` — [List webhook deliveries](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/list-webhook-deliveries) - `GET /v1/webhook-deliveries/{delivery}` — [Get a webhook delivery](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/get-a-webhook-delivery) - `POST /v1/webhook-deliveries/retry-dead` — [Retry every recent dead delivery](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-every-recent-dead-delivery) - `POST /v1/webhook-deliveries/{delivery}/retry` — [Retry a webhook delivery](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-a-webhook-delivery) ## Related - [Webhook Endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints): The targets these deliveries were posted to; pause one while you fix it. - [Webhook Events API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-events): The polling view of recent deliveries by event type. - [Retries & Delivery](https://alm-docs.labs.zuselab.dev/webhooks/v1/retries-and-delivery): The ladder, dead-lettering and auto-disable. - [Testing & Debugging Webhooks](https://alm-docs.labs.zuselab.dev/webhooks/v1/testing-and-debugging): Fire a test payload, read the delivery log and retry a failed delivery by hand, without waiting for the production trigger. --- # List webhook deliveries Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/list-webhook-deliveries `GET /v1/webhook-deliveries` ```bash curl "https://api.example.test/v1/webhook-deliveries?status=dead" \ -H "Authorization: Bearer $TOKEN" ``` The team's deliveries across every endpoint, **newest first**, 15 per page (`per_page` 1 to 100; `limit` is accepted as an alias). `status` narrows to one of the log's tabs. - Requires ability: `webhook-delivery:view-any` - MCP tools: `list_webhook_deliveries` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | `all`, `pending`, `delivered`, `failed`, `dead` \| null | no | One of the log's tabs: `all` (the default), `pending`, `delivered`, `failed` or `dead`. Anything else is refused. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 15. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: The tenant's deliveries on that tab, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The delivery's id, the value the retry endpoint takes. | | `data[].endpoint_id` | string | yes | The webhook endpoint the event was posted to. | | `data[].event` | string | yes | The event name, as the catalogue lists it. | | `data[].event_id` | string | yes | The 26-character ULID the envelope's `id` carries without its `evt_` prefix; also the `ZG-Event-Id` header the handler received. | | `data[].status` | string | yes | `pending` (born, or reset by a retry), `failed` (a non-2xx or transport error while retries remain), `delivered` (the first 2xx) or `dead` (the ladder ran out or the endpoint vanished). `delivered` and `dead` are final until a retry. | | `data[].attempts` | integer | yes | Posts made so far. Reset to 0 by a retry. | | `data[].response_status` | integer \| null | yes | The HTTP status the endpoint returned on the last attempt; null before the first post or after a transport failure. | | `data[].response_excerpt` | string \| null | yes | The start of the response body, or the transport error; null before the first post. | | `data[].payload` | array of any | yes | The full event envelope that was, or would have been, posted, so a consumer can be debugged against it exactly. | | `data[].next_attempt_at` | string \| null | yes | When the worker will post a `failed` row again, ISO 8601; null otherwise. | | `data[].delivered_at` | string \| null | yes | Stamped on the first 2xx, ISO 8601; null until then. | | `data[].dead_lettered_at` | string \| null | yes | Stamped when the ladder ran out, ISO 8601; null otherwise. | | `data[].created_at` | string | yes | When the row was born, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: naming `status` when it is not one of the five tabs. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Retry a webhook delivery Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-a-webhook-delivery `POST /v1/webhook-deliveries/{delivery}/retry` ```bash curl -X POST https://api.example.test/v1/webhook-deliveries/$DELIVERY_ID/retry \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Answers `200` with the row **pending again**: `attempts` back to `0`, `response_status`, `response_excerpt` and `dead_lettered_at` cleared, and the worker queued. The delivery log then shows the replay's own outcome rather than the original failure's. A retry re-posts the **original** event to its endpoint; nothing new is raised about the replay itself. > **Only `failed` and `dead` rows can be retried.** Resetting a delivered row would post the event to your server a second time. The app hides its retry button behind the same rule, but a row can be delivered between a render and a click, and the API has no button at all. - Requires ability: `webhook-delivery:retry` - MCP tools: `retry_webhook_delivery` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `delivery` | path | string (uuid) | yes | The delivery, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The delivery, pending again. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The delivery's id, the value the retry endpoint takes. | | `data.endpoint_id` | string | yes | The webhook endpoint the event was posted to. | | `data.event` | string | yes | The event name, as the catalogue lists it. | | `data.event_id` | string | yes | The 26-character ULID the envelope's `id` carries without its `evt_` prefix; also the `ZG-Event-Id` header the handler received. | | `data.status` | string | yes | `pending` (born, or reset by a retry), `failed` (a non-2xx or transport error while retries remain), `delivered` (the first 2xx) or `dead` (the ladder ran out or the endpoint vanished). `delivered` and `dead` are final until a retry. | | `data.attempts` | integer | yes | Posts made so far. Reset to 0 by a retry. | | `data.response_status` | integer \| null | yes | The HTTP status the endpoint returned on the last attempt; null before the first post or after a transport failure. | | `data.response_excerpt` | string \| null | yes | The start of the response body, or the transport error; null before the first post. | | `data.payload` | array of any | yes | The full event envelope that was, or would have been, posted, so a consumer can be debugged against it exactly. | | `data.next_attempt_at` | string \| null | yes | When the worker will post a `failed` row again, ISO 8601; null otherwise. | | `data.delivered_at` | string \| null | yes | Stamped on the first 2xx, ISO 8601; null until then. | | `data.dead_lettered_at` | string \| null | yes | Stamped when the ladder ran out, ISO 8601; null otherwise. | | `data.created_at` | string | yes | When the row was born, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: for a `pending` or `delivered` row, with the message *Only failed or dead-lettered deliveries can be retried.* - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Retry every recent dead delivery Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-every-recent-dead-delivery `POST /v1/webhook-deliveries/retry-dead` Once the consumer is fixed, replay everything the team dead-lettered since an instant in one call. ```bash curl -X POST https://api.example.test/v1/webhook-deliveries/retry-dead \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"since": "2026-11-02T22:00:00Z"}' ``` `retried` is how many rows were reset and queued. Only `dead` rows are touched: a `failed` row is still on its ladder and will be posted again on its own. The replay is capped at 500 rows per call so one request after a long outage cannot flood the queue or the target; call it again if the count comes back at the ceiling. Each replayed row posts its event again, so confirm the consumer is healthy first; see [replaying dead deliveries in bulk](/webhooks/v1/retries-and-delivery). - Requires ability: `webhook-delivery:retry` - MCP tools: `retry_dead_webhook_deliveries` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) How far back to replay. Optional: omitted, the last 24 hours are replayed, the settings page's replay window. | Field | Type | Required | Description | | --- | --- | --- | --- | | `since` | string \| null (date-time) | no | ISO 8601, not in the future. Only rows dead-lettered **at or after** this instant are replayed. Omit it for the last 24 hours. | ### Responses - **200**: How many rows were re-queued and the instant the window opened at. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.retried` | integer | yes | How many dead rows were reset to pending and queued, at most 500 per call. | | `data.since` | string | yes | The instant the window opened at, ISO 8601: as sent, or 24 hours ago when omitted. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `since` is malformed or in the future. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a webhook endpoint Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/delete-a-webhook-endpoint `DELETE /v1/webhook-endpoints/{endpoint}` ```bash curl -X DELETE https://api.example.test/v1/webhook-endpoints/$ENDPOINT_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Returns `204 No Content`. The endpoint's delivery log stays readable. - Requires ability: `webhook-endpoint:delete` - MCP tools: `delete_webhook_endpoint` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint` | path | string (uuid) | yes | The endpoint, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the caller neither registered the endpoint nor owns the team. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a webhook endpoint Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/get-a-webhook-endpoint `GET /v1/webhook-endpoints/{endpoint}` ```bash curl https://api.example.test/v1/webhook-endpoints/$ENDPOINT_ID \ -H "Authorization: Bearer $TOKEN" ``` The endpoint and its delivery health. The signing secret is never part of it; it exists in the create and rotate-secret responses only. `disabled_at` populates when the worker disables an endpoint after too many consecutive failures, or when someone pauses it. `last_success_at` and `last_failure_at` track the most recent delivery outcomes. `project_id` is `null` for team-wide endpoints. - Requires ability: `webhook-endpoint:view` - MCP tools: `get_webhook_endpoint` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint` | path | string (uuid) | yes | The endpoint, resolved by the route binder. | ### Responses - **200**: The endpoint and its delivery health, without its secret. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The endpoint's id. | | `data.name` | string | yes | The label the endpoint was registered with. | | `data.url` | string | yes | The `https://` URL events are posted to. | | `data.project_id` | string \| null | yes | The project the endpoint is confined to; null for a team-wide endpoint that receives every project's events. | | `data.events` | array of any | yes | The event names the endpoint is subscribed to. | | `data.is_active` | boolean | yes | Whether deliveries flow. False while paused by a person or switched off by the worker after too many consecutive failures. | | `data.disabled_at` | string \| null | yes | When the endpoint was paused or auto-disabled, ISO 8601; null while active. | | `data.allowed_ips` | array \| null | yes | IPv4/IPv6 addresses or CIDR ranges the target must resolve to; a delivery whose host resolves elsewhere is dead-lettered without being posted. Null when unrestricted. | | `data.failure_count` | integer | yes | How many deliveries have failed over the endpoint's lifetime. | | `data.consecutive_failures` | integer | yes | The current failure streak; the worker disables the endpoint when it reaches the limit, and resuming resets it. | | `data.last_success_at` | string \| null | yes | When a delivery last got a 2xx, ISO 8601; null until one has. | | `data.last_failure_at` | string \| null | yes | When a delivery last failed, ISO 8601; null until one has. | | `data.created_at` | string \| null | yes | When the endpoint was registered, ISO 8601. | | `data.updated_at` | string \| null | yes | When the endpoint last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Webhook Endpoints API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints A **webhook endpoint** is a URL of yours that Zuse Bifrost posts events to, with the events it subscribes to and a signing secret you verify each delivery against. An endpoint belongs to the team and may be confined to one project; the secret is shown exactly twice, in the answer to registering the endpoint and in the answer to rotating it, and never appears on a read. Each endpoint carries its delivery health: the last success and failure, the failure streak, and `disabled_at` when the worker switched it off after too many consecutive failures or when someone paused it. **Pause** and **resume** are the on/off switch, and events raised while paused are not queued or replayed; **test** enqueues a synthetic delivery so a new handler can be checked before real traffic; **rotate secret** revokes the old secret at once, so deploy the new one first. The delivery log and the replay of a dead-lettered row live under [webhook deliveries](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries). Every member of the team may list and read endpoints, but only the member who registered one, or the team owner, may rotate, test, pause, resume or delete it. Endpoint lifecycle emits no webhooks of its own, since that would be recursive. ## Who may change an endpoint Every member of the team can list and read the team's endpoints, but only the member who registered an endpoint, or the team owner, may rotate its secret, test it, pause or resume it, or delete it. A token minted by another member receives `403 FORBIDDEN` on those calls; an endpoint outside the token's team is a `404 RESOURCE_NOT_FOUND`, never a `403`, so an id cannot be probed. ## Endpoints - `GET /v1/webhook-endpoints` — [List webhook endpoints](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/list-webhook-endpoints) - `POST /v1/webhook-endpoints` — [Register a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/register-a-webhook-endpoint) - `GET /v1/webhook-endpoints/{endpoint}` — [Get a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/get-a-webhook-endpoint) - `DELETE /v1/webhook-endpoints/{endpoint}` — [Delete a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/delete-a-webhook-endpoint) - `POST /v1/webhook-endpoints/{endpoint}/pause` — [Pause a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/pause-a-webhook-endpoint) - `POST /v1/webhook-endpoints/{endpoint}/resume` — [Resume a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/resume-a-webhook-endpoint) - `POST /v1/webhook-endpoints/{endpoint}/rotate-secret` — [Rotate a webhook endpoint's secret](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/rotate-a-webhook-endpoints-secret) - `POST /v1/webhook-endpoints/{endpoint}/test` — [Test-fire a webhook endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/test-fire-a-webhook-endpoint) ## Related - [Webhook Deliveries API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries): The delivery log, and how to replay a failed or dead-lettered row. - [Webhook Events API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-events): Consume recent deliveries over REST. - [Webhook Security](https://alm-docs.labs.zuselab.dev/webhooks/v1/security): Signatures, public hosts and the IP allow-list. - [Ability Catalogue](https://alm-docs.labs.zuselab.dev/api/v1/abilities): Every ability string a token can carry, how the catalogue mirrors the team permissions, what enforces it on the API today, and how to pick the right ones when minting. --- # List webhook endpoints Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/list-webhook-endpoints `GET /v1/webhook-endpoints` ```bash curl https://api.example.test/v1/webhook-endpoints \ -H "Authorization: Bearer $TOKEN" ``` The team's endpoints without their secrets, newest first, paged with `page` and `per_page` (15 by default). - Requires ability: `webhook-endpoint:view-any` - MCP tools: `list_webhook_endpoints` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 15. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | ### Responses - **200**: The team's endpoints without their secrets. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The endpoint's id. | | `data[].name` | string | yes | The label the endpoint was registered with. | | `data[].url` | string | yes | The `https://` URL events are posted to. | | `data[].project_id` | string \| null | yes | The project the endpoint is confined to; null for a team-wide endpoint that receives every project's events. | | `data[].events` | array of any | yes | The event names the endpoint is subscribed to. | | `data[].is_active` | boolean | yes | Whether deliveries flow. False while paused by a person or switched off by the worker after too many consecutive failures. | | `data[].disabled_at` | string \| null | yes | When the endpoint was paused or auto-disabled, ISO 8601; null while active. | | `data[].allowed_ips` | array \| null | yes | IPv4/IPv6 addresses or CIDR ranges the target must resolve to; a delivery whose host resolves elsewhere is dead-lettered without being posted. Null when unrestricted. | | `data[].failure_count` | integer | yes | How many deliveries have failed over the endpoint's lifetime. | | `data[].consecutive_failures` | integer | yes | The current failure streak; the worker disables the endpoint when it reaches the limit, and resuming resets it. | | `data[].last_success_at` | string \| null | yes | When a delivery last got a 2xx, ISO 8601; null until one has. | | `data[].last_failure_at` | string \| null | yes | When a delivery last failed, ISO 8601; null until one has. | | `data[].created_at` | string \| null | yes | When the endpoint was registered, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the endpoint last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Pause a webhook endpoint Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/pause-a-webhook-endpoint `POST /v1/webhook-endpoints/{endpoint}/pause` ```bash curl -X POST https://api.example.test/v1/webhook-endpoints/$ENDPOINT_ID/pause \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` The on/off switch, off. A paused endpoint keeps its secret, its event subscriptions and its delivery log, but events raised while it is paused are **not** queued for it and are not replayed on resume; use the [deliveries API](/api/v1/reference/webhook-deliveries) to replay rows that dead-lettered before the pause. Answers `200` with the endpoint; idempotent, pausing a paused endpoint changes nothing. - Requires ability: `webhook-endpoint:update` - MCP tools: `pause_webhook_endpoint` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint` | path | string (uuid) | yes | The endpoint, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The endpoint, paused. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The endpoint's id. | | `data.name` | string | yes | The label the endpoint was registered with. | | `data.url` | string | yes | The `https://` URL events are posted to. | | `data.project_id` | string \| null | yes | The project the endpoint is confined to; null for a team-wide endpoint that receives every project's events. | | `data.events` | array of any | yes | The event names the endpoint is subscribed to. | | `data.is_active` | boolean | yes | Whether deliveries flow. False while paused by a person or switched off by the worker after too many consecutive failures. | | `data.disabled_at` | string \| null | yes | When the endpoint was paused or auto-disabled, ISO 8601; null while active. | | `data.allowed_ips` | array \| null | yes | IPv4/IPv6 addresses or CIDR ranges the target must resolve to; a delivery whose host resolves elsewhere is dead-lettered without being posted. Null when unrestricted. | | `data.failure_count` | integer | yes | How many deliveries have failed over the endpoint's lifetime. | | `data.consecutive_failures` | integer | yes | The current failure streak; the worker disables the endpoint when it reaches the limit, and resuming resets it. | | `data.last_success_at` | string \| null | yes | When a delivery last got a 2xx, ISO 8601; null until one has. | | `data.last_failure_at` | string \| null | yes | When a delivery last failed, ISO 8601; null until one has. | | `data.created_at` | string \| null | yes | When the endpoint was registered, ISO 8601. | | `data.updated_at` | string \| null | yes | When the endpoint last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the caller neither registered the endpoint nor owns the team. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Register a webhook endpoint Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/register-a-webhook-endpoint `POST /v1/webhook-endpoints` ```bash curl -X POST https://api.example.test/v1/webhook-endpoints \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Deploy bot", "url": "https://hooks.example.com/zusegenius", "events": ["project.created", "team.member_joined"] }' ``` Answers `201` with the endpoint and, at the top level beside `data`, the plaintext signing `secret`. The token must carry, for every event subscribed, the read ability that event requires; the catalogue lists it per event. > **The secret appears once.** The plaintext `secret` (prefixed `whsec_`) appears at the top level of the response **only** here and on rotate-secret. Subsequent reads never surface it; copy it into your consumer's secret storage immediately. - Requires ability: `webhook-endpoint:create` - MCP tools: `create_webhook_endpoint` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A webhook endpoint to register: where to post, which events, and how to confine it. The signing secret is minted by Zuse Bifrost and returned once beside the endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | A label for the endpoint, 3 to 100 characters. | | `url` | string (uri) | yes | The target, `https://` only, up to 2,048 characters, pointing at a public host: loopback, private, link-local and reserved addresses are refused, as is a public hostname that resolves to one. | | `project_id` | string \| null (uuid) | no | Confine the endpoint to one project's events; omit it for a team-wide subscription. | | `events` | array of `project.created`, `project.updated`, `project.status_changed`, `project.archived`, `project.restored`, `project.deleted`, `project.settings_changed`, `project.member_added`, `project.member_removed`, `component.created`, `component.updated`, `component.archived`, `component.restored`, `component.deleted`, `environment.created`, `environment.updated`, `environment.deleted`, `environment.frozen`, `environment.unfrozen`, `environment.health_changed`, `stakeholder.created`, `stakeholder.updated`, `stakeholder.deleted`, `stakeholder.restored`, `persona.created`, `persona.updated`, `persona.archived`, `persona.restored`, `persona.deleted`, `persona.version_restored`, `glossary_term.created`, `glossary_term.updated`, `glossary_term.status_changed`, `glossary_term.deleted`, `glossary_term.restored`, `data_entity.created`, `data_entity.updated`, `data_entity.status_changed`, `data_entity.fields_changed`, `data_entity.deleted`, `data_entity.restored`, `epic.created`, `epic.updated`, `epic.status_changed`, `epic.restored`, `epic.deleted`, `work_item.created`, `work_item.updated`, `work_item.transitioned`, `work_item.assigned`, `work_item.criteria_changed`, `work_item.restored`, `work_item.quality_reviewed`, `work_item.deleted`, `requirement.created`, `requirement.updated`, `requirement.status_changed`, `requirement.restored`, `requirement.quality_reviewed`, `requirement.deleted`, `business_rule.created`, `business_rule.updated`, `business_rule.status_changed`, `business_rule.restored`, `business_rule.quality_reviewed`, `business_rule.deleted`, `use_case.created`, `use_case.updated`, `use_case.status_changed`, `use_case.steps_changed`, `use_case.restored`, `use_case.deleted`, `raid_item.created`, `raid_item.updated`, `raid_item.status_changed`, `raid_item.restored`, `raid_item.deleted`, `decision.created`, `decision.updated`, `decision.status_changed`, `decision.restored`, `decision.deleted`, `link.created`, `link.deleted`, `comment.created`, `comment.updated`, `comment.resolved`, `comment.reopened`, `comment.deleted`, `label.created`, `label.updated`, `label.deleted`, `attachment.created`, `attachment.deleted`, `design_reference.created`, `design_reference.updated`, `design_reference.deleted`, `repository.created`, `repository.updated`, `repository.disconnected`, `repository.reconnected`, `repository.deleted`, `client.created`, `client.updated`, `client.archived`, `client.restored`, `client.deleted`, `team.created`, `team.updated`, `team.deleted`, `team.member_invited`, `team.member_joined`, `team.member_removed`, `team.member_role_changed`, `role.created`, `role.updated`, `role.deleted`, `token.created`, `token.revoked`, `ai.budget_warning`, `ai.budget_exhausted`, `import.started`, `import.ready`, `import.failed`, `import.discarded`, `import.suggestion_accepted`, `import.suggestion_rejected`, `import.suggestion_merged`, `research.started`, `research.ready`, `research.failed`, `council.convened`, `council.voice_spoke`, `council.round_opened`, `council.ready`, `council.failed`, `council.finding_accepted`, `council.finding_dismissed`, `improvement.swept`, `approval.requested`, `approval.decided`, `approval.approved`, `approval.rejected`, `approval.changes_requested`, `approval.withdrawn`, `approval.escalated`, `approval.policy_updated`, `baseline.created`, `baseline.updated`, `baseline.status_changed`, `baseline.frozen`, `baseline.signed_off`, `baseline.restored`, `baseline.deleted`, `change_request.created`, `change_request.updated`, `change_request.status_changed`, `change_request.analysed`, `change_request.restored`, `change_request.deleted` | yes | The events to subscribe to, at least one. | | `allowed_ips` | array \| null | no | IPv4/IPv6 addresses or CIDR ranges the target must resolve to. Enforced on every delivery: when the endpoint's host resolves to an address outside the list, the delivery is dead-lettered without being posted. | | `is_active` | boolean | no | Whether deliveries flow from the start. Defaults to true. | ### Responses - **201**: 201 with the endpoint and a top-level `secret`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The endpoint's id. | | `data.name` | string | yes | The label the endpoint was registered with. | | `data.url` | string | yes | The `https://` URL events are posted to. | | `data.project_id` | string \| null | yes | The project the endpoint is confined to; null for a team-wide endpoint that receives every project's events. | | `data.events` | array of any | yes | The event names the endpoint is subscribed to. | | `data.is_active` | boolean | yes | Whether deliveries flow. False while paused by a person or switched off by the worker after too many consecutive failures. | | `data.disabled_at` | string \| null | yes | When the endpoint was paused or auto-disabled, ISO 8601; null while active. | | `data.allowed_ips` | array \| null | yes | IPv4/IPv6 addresses or CIDR ranges the target must resolve to; a delivery whose host resolves elsewhere is dead-lettered without being posted. Null when unrestricted. | | `data.failure_count` | integer | yes | How many deliveries have failed over the endpoint's lifetime. | | `data.consecutive_failures` | integer | yes | The current failure streak; the worker disables the endpoint when it reaches the limit, and resuming resets it. | | `data.last_success_at` | string \| null | yes | When a delivery last got a 2xx, ISO 8601; null until one has. | | `data.last_failure_at` | string \| null | yes | When a delivery last failed, ISO 8601; null until one has. | | `data.created_at` | string \| null | yes | When the endpoint was registered, ISO 8601. | | `data.updated_at` | string \| null | yes | When the endpoint last changed, ISO 8601. | | `secret` | string | yes | The plaintext signing secret, `whsec_…`, returned once. Verify every delivery's `ZG-Signature` with it. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `TENANT_MISMATCH`: with `reason: missing_team_scope` when the token carries no team scope. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `url` is not `https://`, points at a private or reserved host, or exceeds 2,048 characters; when `events` is empty or names an unknown event; when `allowed_ips` holds an invalid address or range; when `name` is outside 3 to 100 characters. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Resume a webhook endpoint Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/resume-a-webhook-endpoint `POST /v1/webhook-endpoints/{endpoint}/resume` The switch, on. Resuming also clears the consecutive-failure streak, so a target that was fixed while paused is not disabled again on its first miss. Answers `200` with the endpoint; idempotent. - Requires ability: `webhook-endpoint:update` - MCP tools: `resume_webhook_endpoint` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint` | path | string (uuid) | yes | The endpoint, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The endpoint, active. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The endpoint's id. | | `data.name` | string | yes | The label the endpoint was registered with. | | `data.url` | string | yes | The `https://` URL events are posted to. | | `data.project_id` | string \| null | yes | The project the endpoint is confined to; null for a team-wide endpoint that receives every project's events. | | `data.events` | array of any | yes | The event names the endpoint is subscribed to. | | `data.is_active` | boolean | yes | Whether deliveries flow. False while paused by a person or switched off by the worker after too many consecutive failures. | | `data.disabled_at` | string \| null | yes | When the endpoint was paused or auto-disabled, ISO 8601; null while active. | | `data.allowed_ips` | array \| null | yes | IPv4/IPv6 addresses or CIDR ranges the target must resolve to; a delivery whose host resolves elsewhere is dead-lettered without being posted. Null when unrestricted. | | `data.failure_count` | integer | yes | How many deliveries have failed over the endpoint's lifetime. | | `data.consecutive_failures` | integer | yes | The current failure streak; the worker disables the endpoint when it reaches the limit, and resuming resets it. | | `data.last_success_at` | string \| null | yes | When a delivery last got a 2xx, ISO 8601; null until one has. | | `data.last_failure_at` | string \| null | yes | When a delivery last failed, ISO 8601; null until one has. | | `data.created_at` | string \| null | yes | When the endpoint was registered, ISO 8601. | | `data.updated_at` | string \| null | yes | When the endpoint last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the caller neither registered the endpoint nor owns the team. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Rotate a webhook endpoint's secret Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/rotate-a-webhook-endpoints-secret `POST /v1/webhook-endpoints/{endpoint}/rotate-secret` ```bash curl -X POST https://api.example.test/v1/webhook-endpoints/$ENDPOINT_ID/rotate-secret \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Answers `200` with the endpoint and a brand-new `secret` alongside `data`. The old secret is revoked immediately: deploy the new one to your handler before calling this endpoint. - Requires ability: `webhook-endpoint:update` - MCP tools: `rotate_webhook_endpoint_secret` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint` | path | string (uuid) | yes | The endpoint, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **200**: The endpoint with the new plaintext `secret`, once. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The endpoint's id. | | `data.name` | string | yes | The label the endpoint was registered with. | | `data.url` | string | yes | The `https://` URL events are posted to. | | `data.project_id` | string \| null | yes | The project the endpoint is confined to; null for a team-wide endpoint that receives every project's events. | | `data.events` | array of any | yes | The event names the endpoint is subscribed to. | | `data.is_active` | boolean | yes | Whether deliveries flow. False while paused by a person or switched off by the worker after too many consecutive failures. | | `data.disabled_at` | string \| null | yes | When the endpoint was paused or auto-disabled, ISO 8601; null while active. | | `data.allowed_ips` | array \| null | yes | IPv4/IPv6 addresses or CIDR ranges the target must resolve to; a delivery whose host resolves elsewhere is dead-lettered without being posted. Null when unrestricted. | | `data.failure_count` | integer | yes | How many deliveries have failed over the endpoint's lifetime. | | `data.consecutive_failures` | integer | yes | The current failure streak; the worker disables the endpoint when it reaches the limit, and resuming resets it. | | `data.last_success_at` | string \| null | yes | When a delivery last got a 2xx, ISO 8601; null until one has. | | `data.last_failure_at` | string \| null | yes | When a delivery last failed, ISO 8601; null until one has. | | `data.created_at` | string \| null | yes | When the endpoint was registered, ISO 8601. | | `data.updated_at` | string \| null | yes | When the endpoint last changed, ISO 8601. | | `secret` | string | yes | The new plaintext signing secret, `whsec_…`, returned once. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the caller neither registered the endpoint nor owns the team. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Test-fire a webhook endpoint Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/test-fire-a-webhook-endpoint `POST /v1/webhook-endpoints/{endpoint}/test` Enqueues a synthetic `project.created`-shaped delivery against the endpoint, marked `synthetic: true` in its `data`, and answers `202 Accepted`. The row is handed to the delivery worker immediately, so it shows up under **Settings → Webhook Deliveries** and moves to `delivered` or `failed` as soon as your handler answers. Limited to 10 fires a minute per token. - Requires ability: `webhook-endpoint:update` - MCP tools: `test_webhook_endpoint` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint` | path | string (uuid) | yes | The endpoint, resolved by the route binder. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **202**: 202 with the new delivery's id and status. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.delivery_id` | string | yes | The delivery row to read the outcome from. | | `data.status` | string | yes | Always `pending` at this point; read the delivery for what happened. | | `data.enqueued_at` | string | yes | When the row was handed to the worker, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the caller neither registered the endpoint nor owns the team. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. On this endpoint: `RATE_LIMITED`: past 10 fires a minute; `Retry-After` says when the next one is accepted. --- # Webhook Events API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-events A read-only polling endpoint that surfaces the most recent webhook deliveries for a given event type. The envelope is identical to the live webhook POST body, so polled samples and live hook payloads are indistinguishable. > **Why it exists.** An integration that cannot receive a POST, or one being built before the first live event has fired, can still read real envelopes. If your team has no deliveries for the requested type, the response is an empty `data` array; fall back to the event's reference page for the shape in that case. The endpoint takes `webhook-delivery:view-any`, the same ability as the delivery log. ## Endpoints - `GET /v1/webhook-events` — [Poll recent webhook events](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-events/poll-recent-webhook-events) ## Related - [Webhook Endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints): Create and manage the endpoints whose deliveries feed this list. - [Webhook Deliveries API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries): The same rows with their delivery outcome. - [Event Reference](https://alm-docs.labs.zuselab.dev/webhooks/v1/events): Every event and its payload. - [Ability Catalogue](https://alm-docs.labs.zuselab.dev/api/v1/abilities): Every ability string a token can carry, how the catalogue mirrors the team permissions, what enforces it on the API today, and how to pick the right ones when minting. --- # Poll recent webhook events Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-events/poll-recent-webhook-events `GET /v1/webhook-events` ```bash curl "https://api.example.test/v1/webhook-events?type=team.member_joined&limit=3" \ -H "Authorization: Bearer $TOKEN" ``` The most recent deliveries of one event type, newest first, as the envelopes the endpoints received. `limit` defaults to 3 and is capped at 10; `project_id` restricts to deliveries whose endpoint is confined to that project. `api_version` is the payload-envelope stamp the dispatcher wrote at delivery time; see [versioning](/api/v1/versioning-and-deprecation). Only deliveries against endpoints owned by the token's team are returned; other tenants' events are never surfaced. `data` is empty when no endpoint in your team is subscribed to the requested event type yet, or when no delivery has fired for that type since the relevant endpoint was created. - Requires ability: `webhook-delivery:view-any` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `type` | query | string | yes | The event name to sample, such as `project.created`; must be a case from the outbound webhook event catalogue. An unknown name is refused. | | `limit` | query | integer | no | How many recent deliveries to return, 1 to 10. | | `project_id` | query | string (uuid) | no | Restrict to deliveries whose endpoint is confined to this project. | ### Responses - **200**: Array of `WebhookEventResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | any | yes | The event id, `evt_` followed by a 26-character ULID; the same value the `ZG-Event-Id` header carries without the prefix. | | `data[].type` | string | yes | The event name, such as `project.created`. | | `data[].created_at` | string | yes | When the event was raised, ISO 8601. | | `data[].api_version` | string \| null | yes | The payload-envelope version the dispatcher stamped at delivery time. | | `data[].project_id` | string \| null | yes | The project the event concerns; null for account-level events. | | `data[].data` | string \| array of string | yes | The event's payload, exactly as the event's reference page documents it. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `TENANT_MISMATCH`: with `reason: missing_team_scope` when the token carries no team scope. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `type` is missing or not an event name from the catalogue, `limit` is outside 1 to 10, or `project_id` is not a UUID. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Add a work item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/add-a-work-item `POST /v1/projects/{project}/work-items` ```bash curl -X POST https://api.example.test/v1/projects/$PROJECT_ID/work-items \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "title": "Tier upgrade email", "type": "story", "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "as_a": "gold member", "i_want": "to be told the day I reach platinum", "so_that": "I use the new perks straight away", "priority": "high", "acceptance_criteria": [ { "title": "A member who crosses the platinum threshold gets the email within the hour", "kind": "functional" }, { "title": "A member who falls back below the threshold gets no email", "kind": "negative" } ] }' ``` Only `title` is required. The item takes the next number of the project's tracker sequence and the reference spelled from the project's key, starts in the `backlog` unless `status` says otherwise, `medium` unless `priority` does, and as a `story` unless `type` does; the token holder is its reporter unless `reporter_id` names another member. A sub-task needs a `parent_id`, and only a sub-task or a bug may carry one; the parent must be a story, task, bug or improvement of the same project. The epic, the persona, the environment and every component must be the project's, and every person a member of the team. The fields a kind does not carry are ignored. The save writes version one of the item, criteria included. Answers `201` with the item and emits `work_item.created`. - Requires ability: `work-item:create` - Fires events: `work_item.created` - MCP tools: `create_story_draft` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A work item to add or change. Only `title` is required on a create; the kind defaults to `story`, the priority to `medium` and the status to `backlog`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves through its own endpoint, and `acceptance_criteria` replaces the criteria only when sent. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | yes | What the item is called, 3 to 200 characters. | | `type` | `story`, `task`, `subtask`, `bug`, `spike`, `improvement` | no | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. A sub-task needs `parent_id`; only a sub-task or a bug may carry one. | | `description` | string \| null | no | What it is, as HTML; filtered to a safe subset when read. | | `epic_id` | string \| null (uuid) | no | The epic it hangs under, an epic of the same project; null for none. | | `parent_id` | string \| null (uuid) | no | The item it hangs under, for a sub-task or a bug: a story, task, bug or improvement of the same project. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. Defaults to `medium`. | | `status` | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` | no | Where it starts, on a create only. Defaults to `backlog`. | | `assignee_id` | string \| null (uuid) | no | Who is doing the work, a member of the team; null for nobody. | | `reporter_id` | string \| null (uuid) | no | Who raised it, a member of the team; the token holder when left out on a create. | | `reviewer_id` | string \| null (uuid) | no | Who reviews the code, a member of the team; null for nobody. | | `qa_owner_id` | string \| null (uuid) | no | Who verifies it, a member of the team; null for nobody. | | `story_points` | string \| null | no | The estimate on the project's scale, up to 10 characters, such as `5` or `M`. | | `estimate_minutes` | integer \| null | no | The estimate in minutes, 0 to 100000. | | `remaining_minutes` | integer \| null | no | What is left, in minutes, 0 to 100000. | | `due_on` | string \| null (date) | no | When it is due, `Y-m-d`. | | `persona_id` | string \| null (uuid) | no | The persona a story is written as, a persona of the same project; ignored for other kinds. | | `as_a` | string \| null | no | The "as a" of a story, up to 160 characters; ignored for other kinds. | | `i_want` | string \| null | no | The "I want" of a story, up to 300 characters; ignored for other kinds. | | `so_that` | string \| null | no | The "so that" of a story, up to 300 characters; ignored for other kinds. | | `business_value_points` | integer \| null | no | The value of a story in points, 0 to 1000; ignored for other kinds. | | `severity` | `blocker`, `critical`, `major`, `minor`, `trivial` \| null | no | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; ignored for other kinds. | | `environment_id` | string \| null (uuid) | no | Where a bug was seen, an environment of the same project; ignored for other kinds. | | `component_id` | string \| null (uuid) | no | The component a bug was found in, a component of the same project; ignored for other kinds. | | `steps_to_reproduce` | string \| null | no | How to make a bug happen, as HTML; ignored for other kinds. | | `expected_result` | string \| null | no | What should have happened, as HTML; ignored for other kinds. | | `actual_result` | string \| null | no | What happened, as HTML; ignored for other kinds. | | `reproducible` | `always`, `sometimes`, `once`, `not_reproduced` \| null | no | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; ignored for other kinds. | | `timebox_minutes` | integer \| null | no | A spike's time box in minutes, 0 to 100000; ignored for other kinds. | | `outcome` | string \| null | no | A spike's outcome, as HTML; ignored for other kinds. | | `component_ids` | array \| null | no | The components the item touches, up to twenty components of the same project. | | `label_ids` | array \| null | no | The labels pinned on the item, up to twenty labels of the same project. Left out on a rewrite, the labels stay as they are. | | `acceptance_criteria` | array \| null | no | The criteria to replace the item's with, up to fifty rows of `title` (required), `given`, `when`, `then`, `kind` and an `id` to keep an existing row. Left out, the criteria stay as they are. | | `acceptance_criteria[].id` | string \| null (uuid) | no | The id of an existing criterion to keep and change; leave it out to add a row. | | `acceptance_criteria[].title` | string | yes | The criterion in one sentence a tester can tick, 3 to 300 characters; required. | | `acceptance_criteria[].given` | string \| null | no | The Given of the scenario behind the sentence, up to 500 characters. | | `acceptance_criteria[].when` | string \| null | no | The When of the scenario, up to 500 characters. | | `acceptance_criteria[].then` | string \| null | no | The Then of the scenario, up to 500 characters. | | `acceptance_criteria[].kind` | `functional`, `negative`, `edge`, `non_functional` \| null | no | What the row checks: `functional`, `negative`, `edge` or `non_functional`. Defaults to `functional`. | ### Responses - **201**: The item, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key commits, branches and people use, such as `CRM-142`. | | `data.type` | string | yes | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `data.parent_id` | string \| null | yes | The item it hangs under, for a sub-task or a bug; null otherwise. | | `data.epic_id` | string \| null | yes | The epic it hangs under; null when none. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where it stands: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `data.resolution` | string \| null | yes | Why it closed: `done`, `wont_do`, `duplicate`, `cannot_reproduce` or `deferred`; null while open. | | `data.priority` | string | yes | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.assignee_id` | string \| null | yes | Who is doing the work; null when unassigned. | | `data.assignee_name` | string \| null | yes | That person's name, when the assignee was loaded; null otherwise. | | `data.reporter_id` | string \| null | yes | Who raised it. | | `data.reporter_name` | string \| null | yes | That person's name, when the reporter was loaded; null otherwise. | | `data.reviewer_id` | string \| null | yes | Who reviews the code; null when nobody yet. | | `data.qa_owner_id` | string \| null | yes | Who verifies it; null when nobody yet. | | `data.story_points` | string \| null | yes | The estimate on the project's scale, such as `5` or `M`; null when not sized. | | `data.estimate_minutes` | integer \| null | yes | The estimate in minutes; null when not sized. | | `data.remaining_minutes` | integer \| null | yes | What is left, in minutes; null when not tracked. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.started_at` | string \| null | yes | When work first started, ISO 8601; null until the first move into progress. | | `data.completed_at` | string \| null | yes | When it was finished, ISO 8601; null while open. | | `data.persona_id` | string \| null | yes | The persona a story is written as; null for other kinds. | | `data.as_a` | string \| null | yes | The "as a" of a story; null for other kinds. | | `data.i_want` | string \| null | yes | The "I want" of a story; null for other kinds. | | `data.so_that` | string \| null | yes | The "so that" of a story; null for other kinds. | | `data.business_value_points` | integer \| null | yes | The value of a story, in points; null when not scored. | | `data.severity` | string \| null | yes | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; null for other kinds. | | `data.environment_id` | string \| null | yes | Where a bug was seen; null for other kinds. | | `data.component_id` | string \| null | yes | The component a bug was found in; null for other kinds. | | `data.steps_to_reproduce` | string \| null | yes | How to make a bug happen, as HTML; null for other kinds. | | `data.expected_result` | string \| null | yes | What should have happened, as HTML; null for other kinds. | | `data.actual_result` | string \| null | yes | What happened, as HTML; null for other kinds. | | `data.reproducible` | string \| null | yes | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; null for other kinds. | | `data.timebox_minutes` | integer \| null | yes | A spike's time box, in minutes; null for other kinds. | | `data.outcome` | string \| null | yes | A spike's outcome, as HTML; null for other kinds. | | `data.component_ids` | array of string | yes | The components the item touches, when they were loaded; empty otherwise. | | `data.labels` | array of any | yes | The labels pinned on the item, each with its id, name and colour, when they were loaded; empty otherwise. | | `data.acceptance_criteria` | array of any | yes | The acceptance criteria in order, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's items, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.created_at` | string \| null | yes | When the item was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:create`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when the parent rules are broken, when a reference is not the project's, or when a person is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Assign a work item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/assign-a-work-item `POST /v1/work-items/{item}/assign` ```bash curl -X POST https://api.example.test/v1/work-items/$ITEM_ID/assign \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"assignee_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f"}' ``` Hands the item to a member of the team, or to nobody with `"assignee_id": null`. Answers `200` with the item and emits `work_item.assigned` with the person before and after; a hand-over to the person who already holds it answers `200` and emits nothing. - Requires ability: `work-item:assign` - Fires events: `work_item.assigned` - MCP tools: `assign_work_item` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The item, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The person to hand the work item to, or null to unassign it. | Field | Type | Required | Description | | --- | --- | --- | --- | | `assignee_id` | string \| null (uuid) | yes | The id of the person who will do the work, a member of the team; null to unassign. | ### Responses - **200**: The item, assigned. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key commits, branches and people use, such as `CRM-142`. | | `data.type` | string | yes | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `data.parent_id` | string \| null | yes | The item it hangs under, for a sub-task or a bug; null otherwise. | | `data.epic_id` | string \| null | yes | The epic it hangs under; null when none. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where it stands: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `data.resolution` | string \| null | yes | Why it closed: `done`, `wont_do`, `duplicate`, `cannot_reproduce` or `deferred`; null while open. | | `data.priority` | string | yes | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.assignee_id` | string \| null | yes | Who is doing the work; null when unassigned. | | `data.assignee_name` | string \| null | yes | That person's name, when the assignee was loaded; null otherwise. | | `data.reporter_id` | string \| null | yes | Who raised it. | | `data.reporter_name` | string \| null | yes | That person's name, when the reporter was loaded; null otherwise. | | `data.reviewer_id` | string \| null | yes | Who reviews the code; null when nobody yet. | | `data.qa_owner_id` | string \| null | yes | Who verifies it; null when nobody yet. | | `data.story_points` | string \| null | yes | The estimate on the project's scale, such as `5` or `M`; null when not sized. | | `data.estimate_minutes` | integer \| null | yes | The estimate in minutes; null when not sized. | | `data.remaining_minutes` | integer \| null | yes | What is left, in minutes; null when not tracked. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.started_at` | string \| null | yes | When work first started, ISO 8601; null until the first move into progress. | | `data.completed_at` | string \| null | yes | When it was finished, ISO 8601; null while open. | | `data.persona_id` | string \| null | yes | The persona a story is written as; null for other kinds. | | `data.as_a` | string \| null | yes | The "as a" of a story; null for other kinds. | | `data.i_want` | string \| null | yes | The "I want" of a story; null for other kinds. | | `data.so_that` | string \| null | yes | The "so that" of a story; null for other kinds. | | `data.business_value_points` | integer \| null | yes | The value of a story, in points; null when not scored. | | `data.severity` | string \| null | yes | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; null for other kinds. | | `data.environment_id` | string \| null | yes | Where a bug was seen; null for other kinds. | | `data.component_id` | string \| null | yes | The component a bug was found in; null for other kinds. | | `data.steps_to_reproduce` | string \| null | yes | How to make a bug happen, as HTML; null for other kinds. | | `data.expected_result` | string \| null | yes | What should have happened, as HTML; null for other kinds. | | `data.actual_result` | string \| null | yes | What happened, as HTML; null for other kinds. | | `data.reproducible` | string \| null | yes | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; null for other kinds. | | `data.timebox_minutes` | integer \| null | yes | A spike's time box, in minutes; null for other kinds. | | `data.outcome` | string \| null | yes | A spike's outcome, as HTML; null for other kinds. | | `data.component_ids` | array of string | yes | The components the item touches, when they were loaded; empty otherwise. | | `data.labels` | array of any | yes | The labels pinned on the item, each with its id, name and colour, when they were loaded; empty otherwise. | | `data.acceptance_criteria` | array of any | yes | The acceptance criteria in order, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's items, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.created_at` | string \| null | yes | When the item was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:assign`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `assignee_id` is not a uuid or the person is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Delete a work item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/delete-a-work-item `DELETE /v1/work-items/{item}` ```bash curl -X DELETE https://api.example.test/v1/work-items/$ITEM_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Removes an item with its criteria, its versions and its activity history; the items under it lose their parent and the number it took is never handed out again. Returns `204 No Content` and emits `work_item.deleted`. Prefer a move to `cancelled` with a reason; it keeps the record. - Requires ability: `work-item:delete` - Fires events: `work_item.deleted` - MCP tools: `delete_work_item` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The item, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **204**: No content - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:delete`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a version of a work item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-version-of-a-work-item `GET /v1/work-items/{item}/revisions/{revision}` ```bash curl https://api.example.test/v1/work-items/$ITEM_ID/revisions/$REVISION_ID \ -H "Authorization: Bearer $TOKEN" ``` One version of the item. A version of another artefact, a version the token may not see or a client guest's token is `404 RESOURCE_NOT_FOUND`. - Requires ability: `work-item:view` - MCP tools: `get_work_item` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The item, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version, bound by the middleware. | ### Responses - **200**: The version. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Get a work item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-work-item `GET /v1/work-items/{item}` ```bash curl https://api.example.test/v1/work-items/$ITEM_ID \ -H "Authorization: Bearer $TOKEN" ``` One item with its people's names, the ids of the components it touches and its acceptance criteria in order. An item of a project the token may not see, or an item read through a client guest's token before an approval pinned it, is `404 RESOURCE_NOT_FOUND`. - `type` is `story`, `task`, `subtask`, `bug`, `spike` or `improvement`; `status` is `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`; `priority` is `lowest` to `highest`; `resolution` is set while the item is closed. - The kind's own fields are null for the other kinds. - Requires ability: `work-item:view` - MCP tools: `get_work_item` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The item, bound by the middleware. | ### Responses - **200**: The item with its people, components and acceptance criteria. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key commits, branches and people use, such as `CRM-142`. | | `data.type` | string | yes | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `data.parent_id` | string \| null | yes | The item it hangs under, for a sub-task or a bug; null otherwise. | | `data.epic_id` | string \| null | yes | The epic it hangs under; null when none. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where it stands: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `data.resolution` | string \| null | yes | Why it closed: `done`, `wont_do`, `duplicate`, `cannot_reproduce` or `deferred`; null while open. | | `data.priority` | string | yes | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.assignee_id` | string \| null | yes | Who is doing the work; null when unassigned. | | `data.assignee_name` | string \| null | yes | That person's name, when the assignee was loaded; null otherwise. | | `data.reporter_id` | string \| null | yes | Who raised it. | | `data.reporter_name` | string \| null | yes | That person's name, when the reporter was loaded; null otherwise. | | `data.reviewer_id` | string \| null | yes | Who reviews the code; null when nobody yet. | | `data.qa_owner_id` | string \| null | yes | Who verifies it; null when nobody yet. | | `data.story_points` | string \| null | yes | The estimate on the project's scale, such as `5` or `M`; null when not sized. | | `data.estimate_minutes` | integer \| null | yes | The estimate in minutes; null when not sized. | | `data.remaining_minutes` | integer \| null | yes | What is left, in minutes; null when not tracked. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.started_at` | string \| null | yes | When work first started, ISO 8601; null until the first move into progress. | | `data.completed_at` | string \| null | yes | When it was finished, ISO 8601; null while open. | | `data.persona_id` | string \| null | yes | The persona a story is written as; null for other kinds. | | `data.as_a` | string \| null | yes | The "as a" of a story; null for other kinds. | | `data.i_want` | string \| null | yes | The "I want" of a story; null for other kinds. | | `data.so_that` | string \| null | yes | The "so that" of a story; null for other kinds. | | `data.business_value_points` | integer \| null | yes | The value of a story, in points; null when not scored. | | `data.severity` | string \| null | yes | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; null for other kinds. | | `data.environment_id` | string \| null | yes | Where a bug was seen; null for other kinds. | | `data.component_id` | string \| null | yes | The component a bug was found in; null for other kinds. | | `data.steps_to_reproduce` | string \| null | yes | How to make a bug happen, as HTML; null for other kinds. | | `data.expected_result` | string \| null | yes | What should have happened, as HTML; null for other kinds. | | `data.actual_result` | string \| null | yes | What happened, as HTML; null for other kinds. | | `data.reproducible` | string \| null | yes | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; null for other kinds. | | `data.timebox_minutes` | integer \| null | yes | A spike's time box, in minutes; null for other kinds. | | `data.outcome` | string \| null | yes | A spike's outcome, as HTML; null for other kinds. | | `data.component_ids` | array of string | yes | The components the item touches, when they were loaded; empty otherwise. | | `data.labels` | array of any | yes | The labels pinned on the item, each with its id, name and colour, when they were loaded; empty otherwise. | | `data.acceptance_criteria` | array of any | yes | The acceptance criteria in order, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's items, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.created_at` | string \| null | yes | When the item was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Work Items API Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items A **work item** is one row of the tracker: a story, a task, a sub-task, a bug, a spike or an improvement. All six kinds share the project's tracker sequence, so a reference such as `CRM-142` is always one thing, and each kind adds its own columns to the ones they share: a story's persona and "as a / I want / so that", a bug's severity, environment, steps, expected and actual results, a spike's time box and outcome. A story carries **acceptance criteria**, the sentences that say when it is done, with an optional Given/When/Then each. Every save of an item writes a **version** that also carries its criteria, so an item and its criteria are restored together. The versions are read and put back through the endpoints below, under the item's own abilities. The reads here see the items of the projects the token may see; a client guest's token sees the items an approval pinned, which is none until the approval module lands, because a story is the team's working note until the client signs it off. An item outside that set is a `404 RESOURCE_NOT_FOUND`, never a `403`. Every write runs the same action the Backlog page runs, under the project permissions, and announces itself as a `work_item.*` event. ## Endpoints - `GET /v1/me/work-items` — [List my work items](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-my-work-items) - `GET /v1/projects/{project}/work-items` — [List work items](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-work-items) - `POST /v1/projects/{project}/work-items` — [Add a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/add-a-work-item) - `GET /v1/work-items/{item}` — [Get a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-work-item) - `PATCH /v1/work-items/{item}` — [Update a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/update-a-work-item) - `DELETE /v1/work-items/{item}` — [Delete a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/delete-a-work-item) - `GET /v1/work-items/{item}/revisions` — [List a work item's versions](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-versions) - `GET /v1/work-items/{item}/revisions/{revision}` — [Get a version of a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-version-of-a-work-item) - `GET /v1/work-items/{item}/quality-reviews` — [List a work item's quality reviews](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-quality-reviews) - `POST /v1/work-items/{item}/quality-reviews` — [Review a work item's quality](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/review-a-work-items-quality) - `POST /v1/work-items/{item}/status` — [Move a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/move-a-work-item) - `POST /v1/work-items/{item}/assign` — [Assign a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/assign-a-work-item) - `PUT /v1/work-items/{item}/acceptance-criteria` — [Replace a work item's acceptance criteria](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/replace-a-work-items-acceptance-criteria) - `POST /v1/work-items/{item}/revisions/{revision}/restore` — [Restore a version of a work item](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/restore-a-version-of-a-work-item) ## Related - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics): The epics the items hang under. - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas): The people the stories are written as. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components): What an item touches and where a bug was found. - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity): Who changed an item and when, beside the versions themselves. - [work_item.* events](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item): The lifecycle of a project's work items: a story, task, sub-task, bug, spike or improvement added, changed, moved along its lifecycle, handed to a person, given new acceptance criteria, put back to an earlier version and deleted. --- # List a work item's quality reviews Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-quality-reviews `GET /v1/work-items/{item}/quality-reviews` ```bash curl https://api.example.test/v1/work-items/{item}/quality-reviews \ -H "Authorization: Bearer $TOKEN" ``` Every review Heimdall wrote of the work item, newest first, each with the version it read, its score and band, its summary and its findings. Compare the first review's `version` with the work item's current version from its history: a review of an older version may be stale. A client guest's token is refused. - Requires ability: `work-item:view` - MCP tools: `list_quality_reviews` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The work item, bound by the middleware. | ### Responses - **200**: Array of `QualityReviewResource` | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items. | | `data[].id` | string | yes | The review's id. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].artefact_type` | string | yes | The artefact's type: `work-item`, `requirement` or `business-rule`. | | `data[].artefact_id` | string | yes | The artefact's id. | | `data[].version` | integer \| null | yes | The version of the artefact Heimdall read, as its history numbers them; null when the artefact had no version yet. | | `data[].score` | integer | yes | 0 to 100: 100 less each finding's penalty (25 for `high`, 10 for `medium`, 4 for `low`), never below 0. | | `data[].band` | string | yes | What the score means: `ready` at 80 and above, `needs_work` from 50 to 79, `not_ready` below 50. | | `data[].summary` | string \| null | yes | Two or three sentences on the record as a whole; null when the model gave none. | | `data[].findings` | array of object | yes | The findings, the most serious first; empty when the record was found sound. | | `data[].findings[].code` | string | yes | The kind of problem: `independent`, `negotiable`, `valuable`, `estimable`, `small` or `testable` for an INVEST breach, `ambiguous`, `missing_criteria`, `untestable`, `missing_measure`, `missing_rationale`, `missing_examples`, `incomplete`, `rule_conflict`, `duplicate`, `stack_drift` or `other`. | | `data[].findings[].severity` | string | yes | How much it matters: `high` (a developer or a tester would be blocked or misled), `medium` (usable but would be argued over) or `low` (polish). | | `data[].findings[].message` | string | yes | What is wrong or missing, quoting the words concerned. | | `data[].findings[].suggestion` | string \| null | yes | What to write instead, in the record's own words; null when there is nothing to suggest. | | `data[].findings[].field` | string \| null | yes | The field concerned, such as `title`, `description`, `criteria`, `statement` or `measure`; null for the whole record. | | `data[].findings_count` | integer | yes | How many findings the review carries. | | `data[].provider` | string | yes | The provider that answered, by its configuration key. | | `data[].model` | string | yes | The model that answered. | | `data[].usage_id` | string \| null | yes | The ledger row that filed the call's cost; null when that row is gone. | | `data[].reviewed_by` | string \| null | yes | The person who asked; null when the account is gone. | | `data[].reviewer_name` | string \| null | yes | That person's name, when the reviewer was loaded; null otherwise. | | `data[].reviewed_at` | string | yes | When Heimdall answered, ISO 8601. | | `data[].created_at` | string \| null | yes | When the review was written, ISO 8601. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's, or the holder's role on the project does not carry `work-item:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the work item is hidden from the token or missing. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List a work item's versions Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-versions `GET /v1/work-items/{item}/revisions` ```bash curl "https://api.example.test/v1/work-items/$ITEM_ID/revisions" \ -H "Authorization: Bearer $TOKEN" ``` Every version of the item, newest first, 25 per page (`per_page` 1 to 100). Each carries its `version` number, the `reason` it was written, who wrote it (`author_id`, `author_name`) and through what (`actor_kind`), a one-line `summary`, the `changed_fields`, the `diff` from the version before and the full `snapshot`, whose `acceptance_criteria` key holds the criteria rows as they were. A client guest's token is refused. - Requires ability: `work-item:view` - MCP tools: `get_work_item` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The item, bound by the middleware. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of versions, newest first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The version's id, what a restore names. | | `data[].project_id` | string | yes | The project the artefact belongs to. | | `data[].revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data[].revisable_id` | string | yes | The artefact's id. | | `data[].version` | integer | yes | The version number, from one, in the order the versions were written. | | `data[].reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data[].actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data[].author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data[].author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data[].ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data[].restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data[].restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data[].summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data[].changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data[].diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data[].snapshot` | object | yes | The captured columns as they were after this save. | | `data[].created_at` | string \| null | yes | When the version was written, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token is a client guest's or its holder's role does not carry `work-item:view`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List my work items Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-my-work-items `GET /v1/me/work-items` ```bash curl "https://api.example.test/v1/me/work-items?open=1" \ -H "Authorization: Bearer $TOKEN" ``` The items assigned to the token holder across every project the token may see, with the same paging, sorting and filters as a project's list. Pass `open=1` for what is still to do. - Requires ability: `work-item:view-any` - MCP tools: `list_my_work` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `type` | query | `story`, `task`, `subtask`, `bug`, `spike`, `improvement` \| null | no | Narrow to one kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `status` | query | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` \| null | no | Narrow to one status: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `open` | query | boolean \| null | no | When true, only the items that are neither done nor cancelled. | | `epic_id` | query | string \| null (uuid) | no | Narrow to the items under one epic. | | `parent_id` | query | string \| null (uuid) | no | Narrow to the items under one parent item. | | `assignee_id` | query | string \| null (uuid) | no | Narrow to the items one person is doing. | | `label_id` | query | string \| null (uuid) | no | Narrow to the items carrying one label. | | `priority` | query | `lowest`, `low`, `medium`, `high`, `highest` \| null | no | Narrow to one priority: `lowest`, `low`, `medium`, `high` or `highest`. | | `search` | query | string \| null | no | Words the reference or the title must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of the items assigned to the token holder, across every project the token may see. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The item's id. | | `data[].project_id` | string | yes | The project the item belongs to. | | `data[].number` | integer | yes | Its number in the project's shared tracker sequence. | | `data[].reference` | string | yes | The key commits, branches and people use, such as `CRM-142`. | | `data[].type` | string | yes | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `data[].parent_id` | string \| null | yes | The item it hangs under, for a sub-task or a bug; null otherwise. | | `data[].epic_id` | string \| null | yes | The epic it hangs under; null when none. | | `data[].title` | string | yes | What the item is called. | | `data[].description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data[].status` | string | yes | Where it stands: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `data[].resolution` | string \| null | yes | Why it closed: `done`, `wont_do`, `duplicate`, `cannot_reproduce` or `deferred`; null while open. | | `data[].priority` | string | yes | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. | | `data[].assignee_id` | string \| null | yes | Who is doing the work; null when unassigned. | | `data[].assignee_name` | string \| null | yes | That person's name, when the assignee was loaded; null otherwise. | | `data[].reporter_id` | string \| null | yes | Who raised it. | | `data[].reporter_name` | string \| null | yes | That person's name, when the reporter was loaded; null otherwise. | | `data[].reviewer_id` | string \| null | yes | Who reviews the code; null when nobody yet. | | `data[].qa_owner_id` | string \| null | yes | Who verifies it; null when nobody yet. | | `data[].story_points` | string \| null | yes | The estimate on the project's scale, such as `5` or `M`; null when not sized. | | `data[].estimate_minutes` | integer \| null | yes | The estimate in minutes; null when not sized. | | `data[].remaining_minutes` | integer \| null | yes | What is left, in minutes; null when not tracked. | | `data[].due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data[].started_at` | string \| null | yes | When work first started, ISO 8601; null until the first move into progress. | | `data[].completed_at` | string \| null | yes | When it was finished, ISO 8601; null while open. | | `data[].persona_id` | string \| null | yes | The persona a story is written as; null for other kinds. | | `data[].as_a` | string \| null | yes | The "as a" of a story; null for other kinds. | | `data[].i_want` | string \| null | yes | The "I want" of a story; null for other kinds. | | `data[].so_that` | string \| null | yes | The "so that" of a story; null for other kinds. | | `data[].business_value_points` | integer \| null | yes | The value of a story, in points; null when not scored. | | `data[].severity` | string \| null | yes | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; null for other kinds. | | `data[].environment_id` | string \| null | yes | Where a bug was seen; null for other kinds. | | `data[].component_id` | string \| null | yes | The component a bug was found in; null for other kinds. | | `data[].steps_to_reproduce` | string \| null | yes | How to make a bug happen, as HTML; null for other kinds. | | `data[].expected_result` | string \| null | yes | What should have happened, as HTML; null for other kinds. | | `data[].actual_result` | string \| null | yes | What happened, as HTML; null for other kinds. | | `data[].reproducible` | string \| null | yes | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; null for other kinds. | | `data[].timebox_minutes` | integer \| null | yes | A spike's time box, in minutes; null for other kinds. | | `data[].outcome` | string \| null | yes | A spike's outcome, as HTML; null for other kinds. | | `data[].component_ids` | array of string | yes | The components the item touches, when they were loaded; empty otherwise. | | `data[].labels` | array of any | yes | The labels pinned on the item, each with its id, name and colour, when they were loaded; empty otherwise. | | `data[].acceptance_criteria` | array of any | yes | The acceptance criteria in order, when they were loaded; empty otherwise. | | `data[].position` | integer | yes | Its place among the project's items, from zero. | | `data[].version` | integer | yes | How many times it was approved; zero until the first approval. | | `data[].approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data[].created_at` | string \| null | yes | When the item was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the item last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a filter is malformed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # List work items Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-work-items `GET /v1/projects/{project}/work-items` ```bash curl "https://api.example.test/v1/projects/$PROJECT_ID/work-items?type=story&open=1" \ -H "Authorization: Bearer $TOKEN" ``` The items of one project in the project's order, 25 per page (`per_page` 1 to 100), each with its assignee's and reporter's names and the ids of the components it touches. `sort_by` takes `number`, `reference`, `title`, `type`, `status`, `priority`, `due_on`, `position`, `story_points`, `created_at` or `updated_at`. `type`, `status`, `priority`, `epic_id`, `parent_id` and `assignee_id` narrow the page to one value each, `open` to the items that are neither done nor cancelled, and `search` to the items whose reference or title contain the words. - Requires ability: `work-item:view-any` - MCP tools: `list_work_items` ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project` | path | string (uuid) | yes | The project, bound by the middleware. | | `type` | query | `story`, `task`, `subtask`, `bug`, `spike`, `improvement` \| null | no | Narrow to one kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `status` | query | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` \| null | no | Narrow to one status: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `open` | query | boolean \| null | no | When true, only the items that are neither done nor cancelled. | | `epic_id` | query | string \| null (uuid) | no | Narrow to the items under one epic. | | `parent_id` | query | string \| null (uuid) | no | Narrow to the items under one parent item. | | `assignee_id` | query | string \| null (uuid) | no | Narrow to the items one person is doing. | | `label_id` | query | string \| null (uuid) | no | Narrow to the items carrying one label. | | `priority` | query | `lowest`, `low`, `medium`, `high`, `highest` \| null | no | Narrow to one priority: `lowest`, `low`, `medium`, `high` or `highest`. | | `search` | query | string \| null | no | Words the reference or the title must contain, up to 120 characters. | | `page` | query | integer | no | The 1-based page to return. A page past the last answers an empty `data` array with `meta.total` still filled, so a loop can stop without guessing. | | `per_page` | query | integer | no | Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25. | | `sort_by` | query | string | no | The column to order by. Defaults to `created_at`; a column the endpoint does not offer falls back to the default rather than failing. | | `sort_direction` | query | `asc`, `desc` | no | `asc` or `desc`. Defaults to `desc`. | | `limit` | query | integer | no | Alias of `per_page`, kept for clients that send it. `per_page` wins when both are sent. | ### Responses - **200**: One page of items with their people and components. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | The items on this page. | | `data[].id` | string | yes | The item's id. | | `data[].project_id` | string | yes | The project the item belongs to. | | `data[].number` | integer | yes | Its number in the project's shared tracker sequence. | | `data[].reference` | string | yes | The key commits, branches and people use, such as `CRM-142`. | | `data[].type` | string | yes | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `data[].parent_id` | string \| null | yes | The item it hangs under, for a sub-task or a bug; null otherwise. | | `data[].epic_id` | string \| null | yes | The epic it hangs under; null when none. | | `data[].title` | string | yes | What the item is called. | | `data[].description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data[].status` | string | yes | Where it stands: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `data[].resolution` | string \| null | yes | Why it closed: `done`, `wont_do`, `duplicate`, `cannot_reproduce` or `deferred`; null while open. | | `data[].priority` | string | yes | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. | | `data[].assignee_id` | string \| null | yes | Who is doing the work; null when unassigned. | | `data[].assignee_name` | string \| null | yes | That person's name, when the assignee was loaded; null otherwise. | | `data[].reporter_id` | string \| null | yes | Who raised it. | | `data[].reporter_name` | string \| null | yes | That person's name, when the reporter was loaded; null otherwise. | | `data[].reviewer_id` | string \| null | yes | Who reviews the code; null when nobody yet. | | `data[].qa_owner_id` | string \| null | yes | Who verifies it; null when nobody yet. | | `data[].story_points` | string \| null | yes | The estimate on the project's scale, such as `5` or `M`; null when not sized. | | `data[].estimate_minutes` | integer \| null | yes | The estimate in minutes; null when not sized. | | `data[].remaining_minutes` | integer \| null | yes | What is left, in minutes; null when not tracked. | | `data[].due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data[].started_at` | string \| null | yes | When work first started, ISO 8601; null until the first move into progress. | | `data[].completed_at` | string \| null | yes | When it was finished, ISO 8601; null while open. | | `data[].persona_id` | string \| null | yes | The persona a story is written as; null for other kinds. | | `data[].as_a` | string \| null | yes | The "as a" of a story; null for other kinds. | | `data[].i_want` | string \| null | yes | The "I want" of a story; null for other kinds. | | `data[].so_that` | string \| null | yes | The "so that" of a story; null for other kinds. | | `data[].business_value_points` | integer \| null | yes | The value of a story, in points; null when not scored. | | `data[].severity` | string \| null | yes | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; null for other kinds. | | `data[].environment_id` | string \| null | yes | Where a bug was seen; null for other kinds. | | `data[].component_id` | string \| null | yes | The component a bug was found in; null for other kinds. | | `data[].steps_to_reproduce` | string \| null | yes | How to make a bug happen, as HTML; null for other kinds. | | `data[].expected_result` | string \| null | yes | What should have happened, as HTML; null for other kinds. | | `data[].actual_result` | string \| null | yes | What happened, as HTML; null for other kinds. | | `data[].reproducible` | string \| null | yes | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; null for other kinds. | | `data[].timebox_minutes` | integer \| null | yes | A spike's time box, in minutes; null for other kinds. | | `data[].outcome` | string \| null | yes | A spike's outcome, as HTML; null for other kinds. | | `data[].component_ids` | array of string | yes | The components the item touches, when they were loaded; empty otherwise. | | `data[].labels` | array of any | yes | The labels pinned on the item, each with its id, name and colour, when they were loaded; empty otherwise. | | `data[].acceptance_criteria` | array of any | yes | The acceptance criteria in order, when they were loaded; empty otherwise. | | `data[].position` | integer | yes | Its place among the project's items, from zero. | | `data[].version` | integer | yes | How many times it was approved; zero until the first approval. | | `data[].approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data[].created_at` | string \| null | yes | When the item was added, ISO 8601. | | `data[].updated_at` | string \| null | yes | When the item last changed, ISO 8601. | | `links` | object | yes | Links to the first, last, previous and next pages. | | `links.first` | string \| null | yes | The first page's URL. | | `links.last` | string \| null | yes | The last page's URL. | | `links.prev` | string \| null | yes | The previous page's URL; null on the first page. | | `links.next` | string \| null | yes | The next page's URL; null on the last page. | | `meta` | object | yes | The paging counters for this page. | | `meta.current_page` | integer | yes | The page returned, 1-indexed. | | `meta.from` | integer \| null | yes | The 1-indexed position of this page's first item across every page; null when the page is empty. | | `meta.last_page` | integer | yes | How many pages there are. | | `meta.links` | array of object | yes | Generated paginator links. | | `meta.links[].url` | string \| null | yes | The page's URL; null for the ellipsis and the disabled arrows. | | `meta.links[].label` | string | yes | The link's label: a page number, the previous or next arrow, or an ellipsis. | | `meta.links[].active` | boolean | yes | Whether this link is the current page. | | `meta.path` | string \| null | yes | Base path for paginator generated URLs. | | `meta.per_page` | integer | yes | Number of items shown per page. | | `meta.to` | integer \| null | yes | Number of the last item in the slice. | | `meta.total` | integer | yes | Total number of items being paginated. | - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `type`, `status` or `priority` is not one of its values, an id filter is not a uuid, or `search` is longer than 120 characters. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Move a work item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/move-a-work-item `POST /v1/work-items/{item}/status` ```bash curl -X POST https://api.example.test/v1/work-items/$ITEM_ID/status \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"status": "cancelled", "resolution": "duplicate"}' ``` Moves the item along its lifecycle. From the backlog an item goes to `ready`, `in_progress` or `cancelled`; from ready back to the backlog, on to `in_progress` or to `cancelled`; from in progress to `ready`, `in_review`, `in_qa`, `done` or `cancelled`; from in review to `in_progress`, `in_qa`, `done` or `cancelled`; from in QA to `in_progress`, `done` or `cancelled`; a done item may be reopened to `in_progress`; a cancelled one goes back to the `backlog`. A move into `done` carries the resolution `done`; a move into `cancelled` carries `wont_do` (the default), `duplicate`, `cannot_reproduce` or `deferred`; a move into an open status clears the resolution. The first move into progress stamps `started_at`, a move into done stamps `completed_at`, and the move writes a version with the reason `transition`. Answers `200` with the item and emits `work_item.transitioned` with the status before and after and the resolution; a move to the status the item is in already answers `200` and emits nothing. - Requires ability: `work-item:transition` - Fires events: `work_item.transitioned` - MCP tools: `move_work_item` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The item, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The status to move the work item to, with the resolution a close carries. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` | yes | Where the item goes: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. The move must be one the lifecycle allows from the current status. | | `resolution` | `done`, `wont_do`, `duplicate`, `cannot_reproduce`, `deferred` \| null | no | Why it closes, for `done` (`done`, the default) or `cancelled` (`wont_do`, `duplicate`, `cannot_reproduce` or `deferred`; `wont_do` by default). Ignored for an open status. | ### Responses - **200**: The item, moved. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key commits, branches and people use, such as `CRM-142`. | | `data.type` | string | yes | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `data.parent_id` | string \| null | yes | The item it hangs under, for a sub-task or a bug; null otherwise. | | `data.epic_id` | string \| null | yes | The epic it hangs under; null when none. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where it stands: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `data.resolution` | string \| null | yes | Why it closed: `done`, `wont_do`, `duplicate`, `cannot_reproduce` or `deferred`; null while open. | | `data.priority` | string | yes | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.assignee_id` | string \| null | yes | Who is doing the work; null when unassigned. | | `data.assignee_name` | string \| null | yes | That person's name, when the assignee was loaded; null otherwise. | | `data.reporter_id` | string \| null | yes | Who raised it. | | `data.reporter_name` | string \| null | yes | That person's name, when the reporter was loaded; null otherwise. | | `data.reviewer_id` | string \| null | yes | Who reviews the code; null when nobody yet. | | `data.qa_owner_id` | string \| null | yes | Who verifies it; null when nobody yet. | | `data.story_points` | string \| null | yes | The estimate on the project's scale, such as `5` or `M`; null when not sized. | | `data.estimate_minutes` | integer \| null | yes | The estimate in minutes; null when not sized. | | `data.remaining_minutes` | integer \| null | yes | What is left, in minutes; null when not tracked. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.started_at` | string \| null | yes | When work first started, ISO 8601; null until the first move into progress. | | `data.completed_at` | string \| null | yes | When it was finished, ISO 8601; null while open. | | `data.persona_id` | string \| null | yes | The persona a story is written as; null for other kinds. | | `data.as_a` | string \| null | yes | The "as a" of a story; null for other kinds. | | `data.i_want` | string \| null | yes | The "I want" of a story; null for other kinds. | | `data.so_that` | string \| null | yes | The "so that" of a story; null for other kinds. | | `data.business_value_points` | integer \| null | yes | The value of a story, in points; null when not scored. | | `data.severity` | string \| null | yes | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; null for other kinds. | | `data.environment_id` | string \| null | yes | Where a bug was seen; null for other kinds. | | `data.component_id` | string \| null | yes | The component a bug was found in; null for other kinds. | | `data.steps_to_reproduce` | string \| null | yes | How to make a bug happen, as HTML; null for other kinds. | | `data.expected_result` | string \| null | yes | What should have happened, as HTML; null for other kinds. | | `data.actual_result` | string \| null | yes | What happened, as HTML; null for other kinds. | | `data.reproducible` | string \| null | yes | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; null for other kinds. | | `data.timebox_minutes` | integer \| null | yes | A spike's time box, in minutes; null for other kinds. | | `data.outcome` | string \| null | yes | A spike's outcome, as HTML; null for other kinds. | | `data.component_ids` | array of string | yes | The components the item touches, when they were loaded; empty otherwise. | | `data.labels` | array of any | yes | The labels pinned on the item, each with its id, name and colour, when they were loaded; empty otherwise. | | `data.acceptance_criteria` | array of any | yes | The acceptance criteria in order, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's items, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.created_at` | string \| null | yes | When the item was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:transition`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when `status` is missing or unknown, when the lifecycle refuses the move, or when the resolution does not fit the status. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Replace a work item's acceptance criteria Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/replace-a-work-items-acceptance-criteria `PUT /v1/work-items/{item}/acceptance-criteria` ```bash curl -X PUT https://api.example.test/v1/work-items/$ITEM_ID/acceptance-criteria \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "acceptance_criteria": [ { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c03", "title": "A member who crosses the platinum threshold gets the email within the hour", "kind": "functional" }, { "title": "The email is not sent twice for the same upgrade", "kind": "edge" } ] }' ``` Sends the full list in order: a row with an `id` keeps that row and changes it, a row without one is new, and every row left out is removed; an empty list removes every criterion. The replace writes a version of the item. Answers `200` with the item and its criteria and emits `work_item.criteria_changed` with how many rows were added, changed and removed; a replace that changes nothing answers `200` and emits nothing. - Requires ability: `work-item:update` - Fires events: `work_item.criteria_changed` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The item, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) The full list of acceptance criteria the work item should have, in order. A row with an `id` keeps that row; a row without one is new; rows left out are removed. | Field | Type | Required | Description | | --- | --- | --- | --- | | `acceptance_criteria` | array of object | yes | Up to fifty rows of `title` (required, 3 to 300 characters), `given`, `when`, `then` (each up to 500 characters), `kind` (`functional`, `negative`, `edge` or `non_functional`, `functional` by default) and an `id` to keep an existing row. An empty list removes every criterion. | | `acceptance_criteria[].id` | string \| null (uuid) | no | The id of an existing criterion to keep and change; leave it out to add a row. | | `acceptance_criteria[].title` | string | yes | The criterion in one sentence a tester can tick, 3 to 300 characters; required. | | `acceptance_criteria[].given` | string \| null | no | The Given of the scenario behind the sentence, up to 500 characters. | | `acceptance_criteria[].when` | string \| null | no | The When of the scenario, up to 500 characters. | | `acceptance_criteria[].then` | string \| null | no | The Then of the scenario, up to 500 characters. | | `acceptance_criteria[].kind` | `functional`, `negative`, `edge`, `non_functional` \| null | no | What the row checks: `functional`, `negative`, `edge` or `non_functional`. Defaults to `functional`. | ### Responses - **200**: The item with its criteria. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key commits, branches and people use, such as `CRM-142`. | | `data.type` | string | yes | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `data.parent_id` | string \| null | yes | The item it hangs under, for a sub-task or a bug; null otherwise. | | `data.epic_id` | string \| null | yes | The epic it hangs under; null when none. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where it stands: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `data.resolution` | string \| null | yes | Why it closed: `done`, `wont_do`, `duplicate`, `cannot_reproduce` or `deferred`; null while open. | | `data.priority` | string | yes | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.assignee_id` | string \| null | yes | Who is doing the work; null when unassigned. | | `data.assignee_name` | string \| null | yes | That person's name, when the assignee was loaded; null otherwise. | | `data.reporter_id` | string \| null | yes | Who raised it. | | `data.reporter_name` | string \| null | yes | That person's name, when the reporter was loaded; null otherwise. | | `data.reviewer_id` | string \| null | yes | Who reviews the code; null when nobody yet. | | `data.qa_owner_id` | string \| null | yes | Who verifies it; null when nobody yet. | | `data.story_points` | string \| null | yes | The estimate on the project's scale, such as `5` or `M`; null when not sized. | | `data.estimate_minutes` | integer \| null | yes | The estimate in minutes; null when not sized. | | `data.remaining_minutes` | integer \| null | yes | What is left, in minutes; null when not tracked. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.started_at` | string \| null | yes | When work first started, ISO 8601; null until the first move into progress. | | `data.completed_at` | string \| null | yes | When it was finished, ISO 8601; null while open. | | `data.persona_id` | string \| null | yes | The persona a story is written as; null for other kinds. | | `data.as_a` | string \| null | yes | The "as a" of a story; null for other kinds. | | `data.i_want` | string \| null | yes | The "I want" of a story; null for other kinds. | | `data.so_that` | string \| null | yes | The "so that" of a story; null for other kinds. | | `data.business_value_points` | integer \| null | yes | The value of a story, in points; null when not scored. | | `data.severity` | string \| null | yes | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; null for other kinds. | | `data.environment_id` | string \| null | yes | Where a bug was seen; null for other kinds. | | `data.component_id` | string \| null | yes | The component a bug was found in; null for other kinds. | | `data.steps_to_reproduce` | string \| null | yes | How to make a bug happen, as HTML; null for other kinds. | | `data.expected_result` | string \| null | yes | What should have happened, as HTML; null for other kinds. | | `data.actual_result` | string \| null | yes | What happened, as HTML; null for other kinds. | | `data.reproducible` | string \| null | yes | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; null for other kinds. | | `data.timebox_minutes` | integer \| null | yes | A spike's time box, in minutes; null for other kinds. | | `data.outcome` | string \| null | yes | A spike's outcome, as HTML; null for other kinds. | | `data.component_ids` | array of string | yes | The components the item touches, when they were loaded; empty otherwise. | | `data.labels` | array of any | yes | The labels pinned on the item, each with its id, name and colour, when they were loaded; empty otherwise. | | `data.acceptance_criteria` | array of any | yes | The acceptance criteria in order, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's items, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.created_at` | string \| null | yes | When the item was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a row lacks a title, a clause is too long, a kind is unknown or the list is longer than fifty. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Restore a version of a work item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/restore-a-version-of-a-work-item `POST /v1/work-items/{item}/revisions/{revision}/restore` ```bash curl -X POST https://api.example.test/v1/work-items/$ITEM_ID/revisions/$REVISION_ID/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Puts the version's captured columns and its acceptance criteria back onto the item, status included, and saves. The save writes a new version with the reason `restore` that names the one put back in `restored_from_id`; nothing earlier is rewritten. Answers `201` with the version written and emits `work_item.restored` with both version numbers. - Requires ability: `work-item:update` - Fires events: `work_item.restored` - MCP tools: `restore_revision` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The item, bound by the middleware. | | `revision` | path | string (uuid) | yes | The version to put back, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The version the restore wrote, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The version's id, what a restore names. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.revisable_type` | string | yes | The artefact's family: `epic` today; the planning families join as they land. | | `data.revisable_id` | string | yes | The artefact's id. | | `data.version` | integer | yes | The version number, from one, in the order the versions were written. | | `data.reason` | string | yes | Why it was written: `created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`. | | `data.actor_kind` | string | yes | Through what it was written: `human`, `api_token`, `mcp`, `integration`, `webhook`, `ai` or `system`. | | `data.author_id` | string \| null | yes | The person who wrote it, or on whose behalf Heimdall wrote it; null for the system. | | `data.author_name` | string \| null | yes | That person's name, when the author was loaded; null otherwise. | | `data.ai_feature` | string \| null | yes | The AI feature that wrote it, for a version Heimdall drafted; null otherwise. | | `data.restored_from_id` | string \| null | yes | The version this one put back, for a restore; null otherwise. | | `data.restored_from_version` | integer \| null | yes | That version's number, when the restored version was loaded; null otherwise. | | `data.summary` | string \| null | yes | One line on what the version is: created, the columns edited, the status moved to, the version restored. | | `data.changed_fields` | array of string | yes | The columns this version changed, in snapshot order; empty for the first version. | | `data.diff` | object \| null | yes | The columns that changed, each `{ "from": …, "to": … }`; null for the first version. | | `data.snapshot` | object | yes | The captured columns as they were after this save. | | `data.created_at` | string \| null | yes | When the version was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the version is not the item's. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: when the version is identical to the current record, so there is nothing to restore. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Review a work item's quality Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/review-a-work-items-quality `POST /v1/work-items/{item}/quality-reviews` ```bash curl -X POST https://api.example.test/v1/work-items/{item}/quality-reviews \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" ``` Asks Heimdall to read the work item against INVEST, its acceptance criteria, the project's business rules and its sibling items. The review is kept with its findings, the score they leave (100 less 25 per `high`, 10 per `medium` and 4 per `low` finding, never below 0), its band (`ready` at 80 and above, `needs_work` from 50, `not_ready` below) and the version of the work item it read; the call is filed in the AI ledger under the team's monthly budget. Nothing on the work item changes. Answers `201` with the review and emits `work_item.quality_reviewed`. Read the latest review first: every call spends. - Requires ability: `ai:use` - Fires events: `work_item.quality_reviewed` - MCP tools: `review_quality` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The work item, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Responses - **201**: The review, `201`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The review's id. | | `data.project_id` | string | yes | The project the artefact belongs to. | | `data.artefact_type` | string | yes | The artefact's type: `work-item`, `requirement` or `business-rule`. | | `data.artefact_id` | string | yes | The artefact's id. | | `data.version` | integer \| null | yes | The version of the artefact Heimdall read, as its history numbers them; null when the artefact had no version yet. | | `data.score` | integer | yes | 0 to 100: 100 less each finding's penalty (25 for `high`, 10 for `medium`, 4 for `low`), never below 0. | | `data.band` | string | yes | What the score means: `ready` at 80 and above, `needs_work` from 50 to 79, `not_ready` below 50. | | `data.summary` | string \| null | yes | Two or three sentences on the record as a whole; null when the model gave none. | | `data.findings` | array of object | yes | The findings, the most serious first; empty when the record was found sound. | | `data.findings[].code` | string | yes | The kind of problem: `independent`, `negotiable`, `valuable`, `estimable`, `small` or `testable` for an INVEST breach, `ambiguous`, `missing_criteria`, `untestable`, `missing_measure`, `missing_rationale`, `missing_examples`, `incomplete`, `rule_conflict`, `duplicate`, `stack_drift` or `other`. | | `data.findings[].severity` | string | yes | How much it matters: `high` (a developer or a tester would be blocked or misled), `medium` (usable but would be argued over) or `low` (polish). | | `data.findings[].message` | string | yes | What is wrong or missing, quoting the words concerned. | | `data.findings[].suggestion` | string \| null | yes | What to write instead, in the record's own words; null when there is nothing to suggest. | | `data.findings[].field` | string \| null | yes | The field concerned, such as `title`, `description`, `criteria`, `statement` or `measure`; null for the whole record. | | `data.findings_count` | integer | yes | How many findings the review carries. | | `data.provider` | string | yes | The provider that answered, by its configuration key. | | `data.model` | string | yes | The model that answered. | | `data.usage_id` | string \| null | yes | The ledger row that filed the call's cost; null when that row is gone. | | `data.reviewed_by` | string \| null | yes | The person who asked; null when the account is gone. | | `data.reviewer_name` | string \| null | yes | That person's name, when the reviewer was loaded; null otherwise. | | `data.reviewed_at` | string | yes | When Heimdall answered, ISO 8601. | | `data.created_at` | string \| null | yes | When the review was written, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token does not carry `ai:use`, is a client guest's, or the project's assists are off. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. On this endpoint: `RESOURCE_NOT_FOUND`: when the work item is hidden from the token or missing. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `AI_BUDGET_EXHAUSTED`: when the team's monthly AI budget has no room; nothing is filed. `VALIDATION_FAILED`: when Heimdall could not reach its model; nothing is filed or written, and a retry a moment later is safe. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Update a work item Source: https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/update-a-work-item `PATCH /v1/work-items/{item}` ```bash curl -X PATCH https://api.example.test/v1/work-items/$ITEM_ID \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"priority": "highest", "story_points": "8", "due_on": null}' ``` Send only the fields to change: a field left out keeps its value, and a field sent as `null` is cleared. The status does not change here; the move endpoint owns it. `acceptance_criteria`, when sent, replaces the criteria in one save with the fields; left out, they stay as they are. A save that changes a captured column or the criteria writes a new version. Answers `200` with the item and emits `work_item.updated` with the columns that changed, each with its old and new value; an update that changes no column answers `200` and emits nothing. - Requires ability: `work-item:update` - Fires events: `work_item.updated` - MCP tools: `update_work_item` - Idempotent: the same `Idempotency-Key` replays the original response for 24 hours. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item` | path | string (uuid) | yes | The item, bound by the middleware. | | `Idempotency-Key` | header | string (uuid) | yes | A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with `Idempotent-Replay: true`), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409. | ### Request body (application/json) A work item to add or change. Only `title` is required on a create; the kind defaults to `story`, the priority to `medium` and the status to `backlog`. On an update, a field left out keeps its value and a field sent as null is cleared; the status moves through its own endpoint, and `acceptance_criteria` replaces the criteria only when sent. | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | yes | What the item is called, 3 to 200 characters. | | `type` | `story`, `task`, `subtask`, `bug`, `spike`, `improvement` | no | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. A sub-task needs `parent_id`; only a sub-task or a bug may carry one. | | `description` | string \| null | no | What it is, as HTML; filtered to a safe subset when read. | | `epic_id` | string \| null (uuid) | no | The epic it hangs under, an epic of the same project; null for none. | | `parent_id` | string \| null (uuid) | no | The item it hangs under, for a sub-task or a bug: a story, task, bug or improvement of the same project. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. Defaults to `medium`. | | `status` | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` | no | Where it starts, on a create only. Defaults to `backlog`. | | `assignee_id` | string \| null (uuid) | no | Who is doing the work, a member of the team; null for nobody. | | `reporter_id` | string \| null (uuid) | no | Who raised it, a member of the team; the token holder when left out on a create. | | `reviewer_id` | string \| null (uuid) | no | Who reviews the code, a member of the team; null for nobody. | | `qa_owner_id` | string \| null (uuid) | no | Who verifies it, a member of the team; null for nobody. | | `story_points` | string \| null | no | The estimate on the project's scale, up to 10 characters, such as `5` or `M`. | | `estimate_minutes` | integer \| null | no | The estimate in minutes, 0 to 100000. | | `remaining_minutes` | integer \| null | no | What is left, in minutes, 0 to 100000. | | `due_on` | string \| null (date) | no | When it is due, `Y-m-d`. | | `persona_id` | string \| null (uuid) | no | The persona a story is written as, a persona of the same project; ignored for other kinds. | | `as_a` | string \| null | no | The "as a" of a story, up to 160 characters; ignored for other kinds. | | `i_want` | string \| null | no | The "I want" of a story, up to 300 characters; ignored for other kinds. | | `so_that` | string \| null | no | The "so that" of a story, up to 300 characters; ignored for other kinds. | | `business_value_points` | integer \| null | no | The value of a story in points, 0 to 1000; ignored for other kinds. | | `severity` | `blocker`, `critical`, `major`, `minor`, `trivial` \| null | no | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; ignored for other kinds. | | `environment_id` | string \| null (uuid) | no | Where a bug was seen, an environment of the same project; ignored for other kinds. | | `component_id` | string \| null (uuid) | no | The component a bug was found in, a component of the same project; ignored for other kinds. | | `steps_to_reproduce` | string \| null | no | How to make a bug happen, as HTML; ignored for other kinds. | | `expected_result` | string \| null | no | What should have happened, as HTML; ignored for other kinds. | | `actual_result` | string \| null | no | What happened, as HTML; ignored for other kinds. | | `reproducible` | `always`, `sometimes`, `once`, `not_reproduced` \| null | no | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; ignored for other kinds. | | `timebox_minutes` | integer \| null | no | A spike's time box in minutes, 0 to 100000; ignored for other kinds. | | `outcome` | string \| null | no | A spike's outcome, as HTML; ignored for other kinds. | | `component_ids` | array \| null | no | The components the item touches, up to twenty components of the same project. | | `label_ids` | array \| null | no | The labels pinned on the item, up to twenty labels of the same project. Left out on a rewrite, the labels stay as they are. | | `acceptance_criteria` | array \| null | no | The criteria to replace the item's with, up to fifty rows of `title` (required), `given`, `when`, `then`, `kind` and an `id` to keep an existing row. Left out, the criteria stay as they are. | | `acceptance_criteria[].id` | string \| null (uuid) | no | The id of an existing criterion to keep and change; leave it out to add a row. | | `acceptance_criteria[].title` | string | yes | The criterion in one sentence a tester can tick, 3 to 300 characters; required. | | `acceptance_criteria[].given` | string \| null | no | The Given of the scenario behind the sentence, up to 500 characters. | | `acceptance_criteria[].when` | string \| null | no | The When of the scenario, up to 500 characters. | | `acceptance_criteria[].then` | string \| null | no | The Then of the scenario, up to 500 characters. | | `acceptance_criteria[].kind` | `functional`, `negative`, `edge`, `non_functional` \| null | no | What the row checks: `functional`, `negative`, `edge` or `non_functional`. Defaults to `functional`. | ### Responses - **200**: The item. | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The response's payload. | | `data.id` | string | yes | The item's id. | | `data.project_id` | string | yes | The project the item belongs to. | | `data.number` | integer | yes | Its number in the project's shared tracker sequence. | | `data.reference` | string | yes | The key commits, branches and people use, such as `CRM-142`. | | `data.type` | string | yes | The kind: `story`, `task`, `subtask`, `bug`, `spike` or `improvement`. | | `data.parent_id` | string \| null | yes | The item it hangs under, for a sub-task or a bug; null otherwise. | | `data.epic_id` | string \| null | yes | The epic it hangs under; null when none. | | `data.title` | string | yes | What the item is called. | | `data.description` | string \| null | yes | What it is, as HTML filtered to a safe subset; null when none. | | `data.status` | string | yes | Where it stands: `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done` or `cancelled`. | | `data.resolution` | string \| null | yes | Why it closed: `done`, `wont_do`, `duplicate`, `cannot_reproduce` or `deferred`; null while open. | | `data.priority` | string | yes | How urgent it is: `lowest`, `low`, `medium`, `high` or `highest`. | | `data.assignee_id` | string \| null | yes | Who is doing the work; null when unassigned. | | `data.assignee_name` | string \| null | yes | That person's name, when the assignee was loaded; null otherwise. | | `data.reporter_id` | string \| null | yes | Who raised it. | | `data.reporter_name` | string \| null | yes | That person's name, when the reporter was loaded; null otherwise. | | `data.reviewer_id` | string \| null | yes | Who reviews the code; null when nobody yet. | | `data.qa_owner_id` | string \| null | yes | Who verifies it; null when nobody yet. | | `data.story_points` | string \| null | yes | The estimate on the project's scale, such as `5` or `M`; null when not sized. | | `data.estimate_minutes` | integer \| null | yes | The estimate in minutes; null when not sized. | | `data.remaining_minutes` | integer \| null | yes | What is left, in minutes; null when not tracked. | | `data.due_on` | string \| null | yes | When it is due, `Y-m-d`; null when not agreed. | | `data.started_at` | string \| null | yes | When work first started, ISO 8601; null until the first move into progress. | | `data.completed_at` | string \| null | yes | When it was finished, ISO 8601; null while open. | | `data.persona_id` | string \| null | yes | The persona a story is written as; null for other kinds. | | `data.as_a` | string \| null | yes | The "as a" of a story; null for other kinds. | | `data.i_want` | string \| null | yes | The "I want" of a story; null for other kinds. | | `data.so_that` | string \| null | yes | The "so that" of a story; null for other kinds. | | `data.business_value_points` | integer \| null | yes | The value of a story, in points; null when not scored. | | `data.severity` | string \| null | yes | How badly a bug hurts: `blocker`, `critical`, `major`, `minor` or `trivial`; null for other kinds. | | `data.environment_id` | string \| null | yes | Where a bug was seen; null for other kinds. | | `data.component_id` | string \| null | yes | The component a bug was found in; null for other kinds. | | `data.steps_to_reproduce` | string \| null | yes | How to make a bug happen, as HTML; null for other kinds. | | `data.expected_result` | string \| null | yes | What should have happened, as HTML; null for other kinds. | | `data.actual_result` | string \| null | yes | What happened, as HTML; null for other kinds. | | `data.reproducible` | string \| null | yes | How reliably a bug comes back: `always`, `sometimes`, `once` or `not_reproduced`; null for other kinds. | | `data.timebox_minutes` | integer \| null | yes | A spike's time box, in minutes; null for other kinds. | | `data.outcome` | string \| null | yes | A spike's outcome, as HTML; null for other kinds. | | `data.component_ids` | array of string | yes | The components the item touches, when they were loaded; empty otherwise. | | `data.labels` | array of any | yes | The labels pinned on the item, each with its id, name and colour, when they were loaded; empty otherwise. | | `data.acceptance_criteria` | array of any | yes | The acceptance criteria in order, when they were loaded; empty otherwise. | | `data.position` | integer | yes | Its place among the project's items, from zero. | | `data.version` | integer | yes | How many times it was approved; zero until the first approval. | | `data.approved_at` | string \| null | yes | When it was last agreed, ISO 8601; null while never approved. | | `data.created_at` | string \| null | yes | When the item was added, ISO 8601. | | `data.updated_at` | string \| null | yes | When the item last changed, ISO 8601. | - **400**: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation. - **401**: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (a `zg_test_` token on production). - **403**: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who registered a row answers `FORBIDDEN` with the same status, and says so in its own description. On this endpoint: `FORBIDDEN`: when the token holder's role on the project does not carry `work-item:update`. - **404**: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred. - **409**: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body. - **422**: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a role code already taken or a team the caller does not own, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when a field is malformed, when the parent rules are broken, when a reference is not the project's, or when a person is not a member of the team. - **425**: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed. - **429**: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted. --- # Tenancy & Scopes Source: https://alm-docs.labs.zuselab.dev/api/v1/tenancy-and-scopes Zuse Bifrost is multi-tenant by construction. Every project belongs to a team, and every row that hangs off a project inherits that team's isolation. The API preserves the same boundary: a token minted for Team A can never read or change rows that belong to Team B. ## Request-scoped team Every token carries a mandatory `scope:team:` entry in its ability list. When a request arrives on the API host: 1. The bearer token is validated. 2. The server reads `scope:team:` from the token's ability list and scopes every query to that team for the duration of the request. No database write happens; the override lives in memory only. From that point on, every read the request performs is automatically tenant-scoped, the same isolation the app uses. You never pass `team_id` manually in a request body, except where a resource is created _in_ a team you choose: a token, a role or a group. Missing the `scope:team:` entry on a token fails every request with `TENANT_MISMATCH` before the controller even runs. The settings page appends the scope automatically; only hand-crafted tokens miss it. ## Optional project scope entries To further restrict a token, attach one or more `scope:project:` entries alongside the team scope. The server parses them into an allow-list; any request touching a project outside the list returns `TENANT_MISMATCH` (HTTP 404; see below). Typical use cases: - A pipeline token that reports deployments and test results for one project only. - An MCP token handed to an agent working inside one project. - A read-only reporting pull from a subset of projects. ## Members-only projects A project can be visible to its whole team or to its project members only. The API honours the same rule as the app: a token acting as a person who is not a member of a members-only project does not see it in `GET /v1/projects`, and `GET /v1/projects/{project}` for it is a 404. ## Guests A client stakeholder invited as a guest holds no team-wide abilities. Their token is confined to the projects they were invited to whatever abilities it carries, and it must name one of those projects when minted. Everything internal stays hidden in the service layer, so a guest's token sees what the guest sees in the app and nothing more. ## Cross-team access is 404, not 403 When a token with `scope:team:A` tries to `GET /v1/projects/`, the response is: ```http HTTP/1.1 404 Not Found Content-Type: application/json {"error": {"code": "RESOURCE_NOT_FOUND", ...}} ``` This is intentional: a `403` would leak the fact that the project exists under a different team. `404` keeps tenant existence private. ## MCP inherits the same contract The MCP server enforces the same team and project scoping. A token issued for Team A that tries to exercise a tool against Team B's project returns the same `TENANT_MISMATCH` envelope, wrapped in the MCP response shape. ## Related - [Authentication](/api/v1/authentication): how tokens are minted and presented. - [Ability catalogue](/api/v1/abilities): every accepted ability string plus scope entries. - [Error envelope](/api/v1/errors): `TENANT_MISMATCH` remediation. - [Members and guests](/workspace/members-and-guests): how a guest comes to exist. --- # Versioning & Deprecation Source: https://alm-docs.labs.zuselab.dev/api/v1/versioning-and-deprecation The public API ships under `/v1/*`. `v1` is the only contract currently published. New functionality is added additively; breaking changes will only ever appear in a future major version such as `/v2/*`. ## Additive vs breaking Treat your client as tolerant of additions. The following are **not** breaking and can ship at any time on `v1`: - A new optional request parameter. - A new field in a response body. - A new enum case in an existing enum (a project status, a webhook event, an alert class). - A new endpoint. - A new error code (`error.code`). - A reworded `error.message`. The stable contract is `error.code`, not the free-form message. The following are breaking and will not happen on `v1`: - Removing a field from a response. - Renaming a field. - Changing a field's type. - Adding a new **required** request parameter. - Changing the semantics of an existing parameter or enum case. - Renaming an endpoint path. - Changing an HTTP status code for an existing scenario. ## Webhook payload versioning Outbound webhook envelopes carry an `api_version` field. Consumers that care about payload stability should read it and branch on value changes; additive changes leave the version stamp unchanged. ```json { "id": "evt_01J...", "type": "project.created", "api_version": "2026-10-26", "created_at": "2026-10-26T10:05:00Z", "project_id": "7d1f...", "data": {} } ``` Subscribing to a new event type is never breaking; the envelope shape does not change, only the `type` value. ## When a breaking change is eventually needed If Zuse Bifrost ever needs to break a contract on the REST API, the process will be: 1. A new major version (`/v2/*`) is published alongside `/v1/*`. 2. `v1` continues to operate for a long transition window so existing integrations do not break. 3. Affected responses carry `Sunset` and `Deprecation` headers per [RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594) as the window narrows. 4. After the window closes, `v1` is removed and callers receive `410 Gone`. This page will be updated when a v2 contract ships. Today, `v1` is the only version. ## Related - [Error envelope](/api/v1/errors): stable `error.code` strings. --- # Board Source: https://alm-docs.labs.zuselab.dev/building/board By the end of this page you will know what the board shows and for which scope, how a drag moves a card along its lifecycle and into its place in the order, what a column's limit does and why a drop is sometimes refused, how the swimlanes and the quick filters narrow the view, and what an agent reads when it asks for the board. The **board** is the [backlog](/planning/backlog) seen as columns: one column per work item status, from **Backlog** on the left to **Done** and **Cancelled** on the right, with a card for each item. It has no record of its own. A card is a work item, a column is a status, the order of the cards in a column is the order of the backlog, and the columns the project hides and the limits it sets come from the [project settings](/projects/project-settings). Dragging a card to another column is the same move the backlog's **Move to** makes, with the same lifecycle, the same versions and the same event; dragging it up or down changes its place in the project's order, which the backlog shows too. The board is the team's: a client guest never opens it. ## Before you begin | To | You need | | ----------------------------- | -------------------------------------------------------------------------------------- | | Open the board | `work-item:view-any`; a viewer, an analyst, a tester and a release manager read it | | Drag a card to another column | `work-item:transition`; the project manager, the developers and the testers by default | | Drag a card to a new place | `work-item:rank`; the project manager and the developers by default | | Take a card | `work-item:assign` | | Hide a column or set a limit | `project:update`, under the project's settings | A running [sprint](/building/sprints) gives the board its default scope. Without one, the board shows every open item of the project. ## Open the board [step] **Board** in the project's sidebar, between **Backlog** and **Sprints**, opens the page. The columns run left to right in the order of the lifecycle: Backlog, Ready, In progress, In review, In QA, Done and Cancelled, minus the columns the project hides. Each column's header shows its name, how many items it holds and, when the project has set one, its limit as _held / limit_, with the story points in the column beside them. When the scope is a sprint, a card above the columns names it with its status, its dates and the days left, the points done against the points planned, how many cards the quick filters leave of the scope, and a button that opens the sprint on the Sprints page. For the other scopes a line says what the board is showing. A project with nothing on the board reads _Nothing on the board yet_ with a word on why: the sprint holds no items, every open item is planned into a sprint, or the backlog is empty. The buttons there open the backlog and the sprints. ## Pick what the board shows [step] The first control above the columns is the **scope**: - **Running sprint** is the default: the items of the sprint that is active, in every column. When no sprint runs, the board falls back to all open work. - A **sprint by name** from the list of sprints that are planned, with the team or running, so a planned sprint can be looked over before it starts. - **All open work**: every open item of the project, and the items finished or dropped in the last two weeks, so a done card does not vanish the moment it is dropped in its column. Teams that run without sprints live here. - **Unplanned work**: the items in no sprint, open or lately finished, which is what sprint planning draws from. A link that names a sprint the project no longer has falls back to the running sprint. The second control chooses the **swimlanes**. With none, the board is one row of columns. **By epic** lays a row out per [epic](/planning/epics), named by its reference and title, with the items under no epic last; **by person** lays one out per assignee by name, with the unassigned items last; **by kind** lays one out per work item kind in the order of the lifecycle's kinds. The column headers stay on top; each lane repeats the columns under its own heading, and a card dragged in another lane's column changes its status, never its epic, its person or its kind. The **quick filters** narrow the cards without changing the tallies in the headers: a search over the reference and the title, a **kind**, **Mine** for the cards assigned to you and **Unassigned** for the cards nobody holds. **Clear Filters** drops them all. When the filters leave no card, the board says so under the columns and offers the same button; the headers keep counting every item of the scope, because a limit counts the column, not your view of it. The scope, the swimlanes and the filters live in the page's address, so a link to the board carries them. ## Drag a card [step] Pick a card up and drop it in a column. While it is in the air, the column under the pointer lights up, the card it would land above shows a ring, and the card you hold is drawn as a dashed ghost in its old place. Dropping it does two things at once: 1. **Moves the item** when the column differs from its own status, exactly as **Move to** on the backlog does. The lifecycle decides what is allowed from where the item stands; a move into **Done** resolves it as done, a move into **Cancelled** records it as won't do, the first move into progress stamps when work started, and every move writes a version with the reason _moved_. A drop the lifecycle refuses leaves the card where it was and says why in a toast, as does a drop into a full column. 2. **Places the item** above the card it landed on, or below the last card when it landed at the bottom. The place is the item's position in the project's order, the one the backlog's default sort reads, so a card dragged to the top of **Ready** is also the first ready item on the backlog. Positions are kept with room between them, so one drag writes one row; when the room runs out the project's order is renumbered in the same move. Placing an item writes no version, because its position is not part of its record. A card dropped in its own column only changes place; the toast then reads _keeps its column at its new place in the order_. A card dropped in an empty column only changes status. The board draws itself again from what the server kept, so a drop that was refused never leaves a card where it cannot be. The drag is the browser's own, which touch screens do not offer, so the card's menu does the same work: **Move to** lists the columns the lifecycle allows and the board shows, and **Open in Backlog** opens the item's flyout, where the order can be changed by hand once the backlog offers it. The drag never touches a card the viewer may neither move nor place. ## Move and take a card from its menu [step] The dots on a card open its menu: - **Open in Backlog** opens the item's flyout on the backlog, with its fields, criteria, links, comments, files and versions. - **Move to** offers the columns the lifecycle allows from where the item stands, among the columns the board shows; a column the project hides is not offered here, though the backlog's **Move to** still reaches it. - **Assign to Me** hands the card to you, when it is not yours already; `work_item.assigned` goes out and the person who held it before is named in it. The card itself shows the reference with the kind's colour, the title (a link to the backlog), the epic when the lane does not already say it, the priority, the story points, the sprint when the scope is not a sprint, and the assignee's initials or _Unassigned_. ## Keep a column under its limit [step] A **work-in-progress limit** caps an open column. The project's settings carry one per status under **Board limits and hidden columns**, and the header shows it as _held / limit_, turning red when the column holds as many as the limit allows. From that moment every move into the column is refused, from a drag, from the menu, from the backlog, from an agent and from the API alike, with a message that names the column, what it holds and the limit; the count is the project's, across every sprint, because the limit is about the team's load rather than one sprint's. Finish or move something out, and the column takes cards again. The closed columns, Done and Cancelled, take no limit. **Hidden columns** take a status off the board entirely. Items in a hidden status stay in the backlog and in the counts the backlog shows; they are simply not drawn here, and a card's **Move to** does not offer a hidden column. Hide **Cancelled** to keep the dropped work out of sight, or **In review** when the team does not review code in the tracker. ## All the fields The board has no form of its own; its controls and the parts of a card are these. **Running sprint** by default, a planned or running sprint by name, **All open work** or **Unplanned work**. Carried in the address as `scope`. What each shows is described under _Pick what the board shows_ above. **No swimlanes**, **By epic**, **By person** or **By kind**. Carried as `lane`. The lanes group the cards; the column headers and their tallies stay the same. The quick filters. The search matches the reference and the title; **Kind** is one of story, task, sub-task, bug, spike or improvement; **Mine** keeps the cards assigned to you; **Unassigned** keeps the cards nobody holds. They narrow the cards, not the tallies. Drops the search, the kind, **Mine** and **Unassigned** at once; the scope and the swimlanes stay. The status with its icon, the story points in the column, and the count: alone when the column has no limit, as _held / limit_ when it has one, red once the column is full. The reference in the kind's colour, the title, the epic's reference, the priority, the story points, the sprint outside a sprint scope, the assignee's initials, and the menu with **Open in Backlog**, **Move to** and **Assign to Me**. The card carries the work item's own fields; the [backlog](/planning/backlog) describes every one. ## What happens next - A drag into another column, or **Move to**, runs the same move as the backlog: the item's status, resolution and stamps change, a version is written with the reason _moved_, and `work_item.transitioned` goes out with the status before and after. - A drag to a new place writes the item's position and `work_item.ranked` goes out with it; a drop that leaves the position as it was emits nothing, and the items renumbered around it when the order ran out of room emit nothing of their own. - **Assign to Me** writes the assignee and `work_item.assigned` goes out with the person before and after; the backlog's My Work and an agent's `list_my_work` read the hand-over at once. - The running sprint's points done and planned, on this page and on the Sprints page, follow the moves into and out of **Done**. ## Heimdall on this page The board has no assist of its own; the cards are the backlog's items, and the assists on their fields live in the backlog's editor. Heimdall's panel reads the board the way an agent does, so ask it what is in progress for the running sprint, which columns are full, what nobody holds, or what has sat in review longest; its answer names the cards and opens them. ## From the API and MCP ## Permissions | Action | Permission | | ----------------------------- | ------------------------------------------------------------------------ | | Open the board | `work-item:view-any`; never a client guest | | Drag a card to another column | `work-item:transition` | | Drag a card to a new place | `work-item:rank`; without it a drag still moves the card between columns | | Take a card | `work-item:assign` | | Read the board as an agent | `work-item:view-any`, as staff | | Rank an item over the API | `work-item:rank` | | Hide a column or set a limit | `project:update` | ## Frequently asked **My drop was refused. Why?** Two reasons, and the toast names which. The lifecycle does not allow the move from where the item stands, as from **Ready** straight into **Done**; or the column is full, which its red _held / limit_ shows. Move the card through the steps the lifecycle allows, or make room in the column. **Where did my done card go?** A sprint scope keeps its done cards for the life of the sprint. **All open work** and **Unplanned work** keep a finished or dropped card for two weeks after it was finished, then let it go; the backlog keeps it for good. **Does dragging on the board change the backlog?** Yes, in both directions. A card's column is the item's status, which the backlog shows; a card's place is the item's position, which the backlog's default sort follows. A sort by another column on the backlog is a view, and does not change the order. **Does the limit count only the sprint I am looking at?** No. The limit counts every item of the project in that status, whatever sprint it is in or none, because it caps what the team has in hand. The header shows the same count in every scope. **Why did the positions of other cards change after one drag?** Positions are whole numbers kept with room between them. When two neighbours have no room left, the project's order is written again with the room restored, the item in its new place and nothing else in a different order. The other items' positions change, their records do not, and only the dragged item's `work_item.ranked` goes out. ## Related - [Backlog](/planning/backlog): the items on the cards, their fields, their flyout and the same moves in a list. - [Sprints](/building/sprints): the scope the board follows by default, and the points it shows. - [Project settings](/projects/project-settings): the hidden columns and the limits. - [Epics](/planning/epics): the swimlanes by epic. - [Revisions and versions](/planning/revisions-and-versions): the version every move writes. --- # Building Source: https://alm-docs.labs.zuselab.dev/building The build phase reads the planning record the business analyst wrote and turns it into shipped work. The [backlog](/planning/backlog) already holds the stories, tasks, bugs, spikes and improvements; building is how the team takes them on in order: a **sprint** is planned with its dates and a goal, takes its scope from the backlog, starts when the team commits to it, and closes with the points it finished kept as its velocity and the unfinished work carried on. The **board** shows that scope as columns, one per status, and a card dragged between them is the same move the backlog makes. Everything the team does in a sprint stays on the items themselves, so the record a tester, a release manager or a client reads later is the same record the developer worked from. | Piece | What it is | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sprint** | A dated run of work numbered within the project, with a goal, a capacity in points and hours, a scope of work items, the team's commitment that starts it, the points it committed to and finished, its velocity and a retrospective. One sprint of a project runs at a time. | | **Board** | The running sprint's items, or any scope of the backlog, in a column per status: dragged along the lifecycle and into their place in the order, under the limits each column keeps, laid out by epic, person or kind. | The definitions of ready and done, time tracking, the Git links and the CI reports join this section as each module lands, and each gets its own page here. A work item carries the sprint it is planned into beside its epic, so the backlog filters by sprint, the sprint's flyout lists its scope, the board follows the running sprint, and an agent plans an item through the item itself. The commitment that starts a sprint is an [approval](/planning/approvals) like any other, decided by the people the project's policy names. --- # Sprints Source: https://alm-docs.labs.zuselab.dev/building/sprints By the end of this page you will know how a sprint is planned and dated, how its scope is pulled from the backlog, how the team's commitment starts it and what that approval pins, what the page shows while it runs, how it is closed with its velocity and where the unfinished work goes, and how a sprint is cancelled or deleted. A **sprint** is a dated run of work the team commits to. It is numbered within the project (_Sprint 1_, _Sprint 2_), given a goal and a capacity in points and hours, and filled with work items from the [backlog](/planning/backlog) as its **scope**. A planned sprint is put to the team through the **Sprint commitment** [approval](/planning/approvals); when that carries, the sprint is **active**, stamped with the points it committed to, and the one running sprint of the project. Closing it records the points of the items done as its **velocity** and carries the unfinished items to the backlog or the next planned sprint. Every save of a sprint's name, goal, dates, capacity or retrospective keeps the [version](/planning/revisions-and-versions) before it. Sprints are the team's: a client guest never opens the page. ## Before you begin | To | You need | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | | See the sprints and open one | `sprint:view-any` and `sprint:view`; a viewer, an analyst, a tester and a release manager read them | | Plan a sprint and change it | `sprint:create`, `sprint:update`; the project manager and the developers by default | | Plan the scope | `work-item:update` | | Put a sprint to the team | `sprint:start` | | Commit | To be named by the project's **Sprint commitment** policy; the project manager and developer roles by default, any one of them | | Close a sprint | `sprint:close` | | Cancel a sprint | `sprint:update` | | Delete a planned or cancelled sprint | `sprint:delete` | The project's **sprint length** under [project settings](/projects/project-settings) dates a new sprint; two weeks unless the project says otherwise. ## Open the page [step] **Sprints** in the project's sidebar opens the page; the badge on the entry counts the sprints still ahead or running. When a sprint is running, a card above the list shows it first: its name and number, its dates and the days left, how many items are done of its scope, the points done against the points planned, what it committed to and its capacity, with a bar for the progress and the buttons to view it, plan its scope and close it. The list shows each sprint's name and number with the first line of its goal, its dates with the days left while it is open, its status, how many items it holds, its points (what it committed to, and what it finished once closed) and its capacity. Sort by number, dates or status; narrow the list by status or search by name; **Clear Filters** drops every filter. An empty project reads _No Sprints Yet_ and, to someone who may plan: _Plan the first one with the button above: name it, date it, pull its scope from the backlog, and put it to the team; their commitment starts it._ ## Plan a sprint [step] Press **New Sprint**. The editor asks for a **Name** (_Sprint 3_), a **Goal** in a sentence or two, the dates (**Starts on** is today and **Ends on** the start plus the project's sprint length, both changeable), and the capacity: **Capacity in points**, how many story points the team expects to finish, and **Capacity in hours**, how many hours it has after leave and other work. Press **Plan Sprint**. The sprint is written as **Planned**, numbered within the project, and the toast reads _Sprint planned_. A planned or running sprint changes through **Edit Sprint** in the row menu or **Edit** in the flyout: the name, the goal, the dates, the capacity and, while it runs, the **Retrospective**. **Save Changes** writes a version. A sprint with the team for its commitment does not change until they answer, and a completed or cancelled one never does. ## Plan the scope [step] **Plan Scope** in the row menu, the flyout or the running sprint's card opens the planner on the open backlog items not yet in a sprint, each with its reference, title, kind, status, points and assignee. Tick the items and press **Add to Sprint**; an item joins at the points it carries today. The flyout's **Scope** table lists what the sprint holds, and the cross on a row sends that item back to the backlog. An item that is done or cancelled does not join; a sprint that is with the team, completed or cancelled takes no more items and gives none back. The [backlog](/planning/backlog) plans from its own side too: the **Sprint** submenu on an item's row and the **Sprint** field in its editor move one item into a planned or running sprint or back to the backlog, and the backlog's sprint filter shows one sprint's items or the items in no sprint. A project has at most one running sprint. A planned sprint cannot be put to the team while another runs; close or cancel the running one first. Items may wait in several planned sprints at once. ## Put it to the team and start it [step] Open a planned sprint's flyout. Once it holds at least one item and has both dates, the **Approval** bar offers **Submit for review**: write a few lines for the team under **Summary** and confirm. The sprint moves to **Awaiting commitment**, the approvers of the project's **Sprint commitment** policy are told (the project manager and the developers by default, any one of them carrying it, with a day to answer), and the scope holds until they do. What the commitment does when it carries: - The sprint becomes **Active** at version 1, stamped with when it started, who put it to the team and the points its scope held, which stay as **Committed** however the scope moves afterwards. - The running sprint's card appears above the list, and the backlog shows the sprint on each of its items. - `sprint.started` goes out beside `sprint.status_changed`, with the approval that carried it, the points committed to and the number of items. A rejection returns the sprint to **Planned** with the approver's reason on the request; a request for changes holds it until you resubmit. A project whose commitment gate is switched off starts the sprint at once, as an approval that carried by itself. ## Follow it while it runs [step] Open a running sprint for its flyout: the dates and the days left, the capacity, the points done against the points planned with what was committed, when it started and by whom, the goal, and the **Scope** table with each item's status, points and assignee. Items still join and leave a running sprint; the committed points keep the record of what the team took on. The sprint's own **Versions** and **Activity** sit below. The board and the burndown join this flyout when they land. ## Close it [step] **Close Sprint** in the row menu, the flyout or the running sprint's card opens the close dialog with the count of items done, the points done that become the velocity, and the count of unfinished items. Choose where the unfinished items go under **Carry the unfinished items to**: a planned sprint of the project, or _Back to the backlog_ when left empty. Write the **Retrospective** (or leave it empty to keep what was written during the sprint) and press **Close Sprint**. The sprint becomes **Completed**: its **completed points** and **velocity** are the points of the items done, when it closed and who closed it are stamped, the unfinished items move where you chose, and `sprint.closed` goes out beside `sprint.status_changed` with the velocity, how many items were carried and where. A completed sprint is the record of what the team did: it is never edited, rendered or deleted. ## Cancel or delete a sprint [step] A planned or running sprint that will not happen is cancelled from **Move to → Cancelled** in the row menu or the flyout; its items go back to the backlog and the sprint stays readable. The moves through **Awaiting commitment**, **Active** and **Completed** belong to the commitment and the close, and the menu does not offer them. **Delete Sprint** removes a planned or cancelled sprint with its versions and activity; its items go back to the backlog. A sprint the team committed to, running or completed is refused: cancel or close it instead. ## All the fields 2 to 120 characters; how the team, the board and the backlog name the sprint. Changed while the sprint is planned or running. Rich text up to 20,000 characters, optional: what the sprint sets out to reach. Shown in the flyout and, in its first line, under the name in the list; the commitment request carries it as its title. The first and last day, both included. A new sprint starts today and ends the project's sprint length later unless you pick other dates; the end is on or after the start. Both must be set before the sprint is put to the team. Optional. The story points the team expects to finish (0 to 9,999, halves allowed) and the hours it has (0 to 100,000). Read beside the points planned and committed; the velocity of the closed sprints is the fair guide for the next. Rich text up to 20,000 characters, optional: what the team learned. Written in the editor while the sprint runs or in the close dialog, and kept with the sprint afterwards. Read-only. The status moves through the commitment (planned, awaiting commitment, active), the close (completed) and by hand only to cancelled. The number is the sprint's place within the project. The version is 0 while never committed to and 1 once the commitment carried. Read-only. The items the sprint holds with their status, points and assignee; how many there are; the points of all of them and of the ones done so far; the points the scope held when the team committed; and, once closed, the points done, kept as the velocity. Read-only. How many days the sprint spans and how many remain until it ends (negative once past); when it started and who put it to the team; when it was closed and by whom. Read-only: the sprint's id, when it was planned and when it last changed, shown in your own timezone. The backlog items ticked in the planning dialog, at least one and at most 500, each an open item of the project not yet in a sprint. Where the unfinished items go, a planned sprint of the project or the backlog when empty; and the retrospective to keep, or empty to keep what the sprint already holds. ## What happens next - Planning a sprint emits `sprint.created`; every change to its words, dates or capacity `sprint.updated` with what changed; planning the scope `sprint.scope_changed` with the references that joined and left. - The commitment emits `approval.requested`, the verdicts and `sprint.status_changed` on every move; the start adds `sprint.started` with the approval, the points committed to and the item count. - Closing emits `sprint.status_changed` and `sprint.closed` with the velocity and what was carried; cancelling emits `sprint.status_changed`; deleting emits `sprint.deleted`; putting an earlier version back emits `sprint.restored`. - An item planned into a sprint from the backlog announces itself as `work_item.updated` with the sprint that changed, and shows the sprint on its row and in its flyout. - The sidebar badge counts the open sprints; the board, the burndown and the time logs read the running sprint as those modules land. ## Heimdall on this page Heimdall reads sprints and plans into them when asked: _"What is in the running sprint and how far along is it?"_ reads the scope and the points done against the commitment; _"Plan the password stories into Sprint 3"_ moves the items through their own sprint field; _"What velocity did we reach in the last sprints?"_ reads the closed ones. Heimdall never plans, starts, closes or cancels a sprint itself; those stay with the team. ## From the API and MCP ## Permissions | Action | Permission | | ------------------------------------------ | ------------------------------------------------------------------------- | | See the sprints and open one | `sprint:view-any`, `sprint:view`; never a client guest | | Plan a sprint, change it, cancel it | `sprint:create`, `sprint:update` | | Plan the scope | `work-item:update` | | Put a sprint to the team | `sprint:start`, as staff | | Commit | Named by the **Sprint commitment** policy; `approval:decide` over the API | | Close a sprint | `sprint:close` | | Delete a planned or cancelled sprint | `sprint:delete` | | Read and restore the sprint's own versions | `sprint:view`, `sprint:update`; on a planned or running sprint | ## Frequently asked **Can two sprints run at once?** No. A sprint cannot be put to the team while another runs. Teams that work in parallel streams run one sprint and tell the streams apart with labels or epics on the items. **The scope changed after the team committed. Which number counts?** Both are kept. **Committed** is the points the scope held when the commitment carried, and stays as the record of what the team took on; the points planned and done follow the scope as it stands, so the gap between them is the scope change. **An item was carried into the next sprint. Does it keep its status?** Yes. Carrying over changes the item's sprint and nothing else; it keeps its status, its assignee and its points, and the closed sprint keeps the items that were done. **We do not vote on commitments; can the sprint just start?** Switch the **Sprint commitment** gate off under the project's approval policies, or name a single approver. With the gate off, submitting starts the sprint at once; the submission is still what stamps the committed points. ## Related - [Backlog](/planning/backlog): the items a sprint takes on, and the sprint field and filter there. - [Approvals](/planning/approvals): the Sprint commitment policy, the inbox and the verdicts. - [Project settings](/projects/project-settings): the sprint length that dates a new sprint. - [Revisions and versions](/planning/revisions-and-versions): every save of a sprint, compared and put back. --- # Brief import Source: https://alm-docs.labs.zuselab.dev/copilot/brief-import By the end of this page you will know how Heimdall's brief reader thinks: what it is given, how it is told to draft each kind of record, how its answer becomes the cards on the review queue, how the reading is metered, and the lines it never crosses. The task itself, from New Import to the last decision on the queue, is on the [Brief import](/planning/brief-import) page of the planning guide. ## What Heimdall is given The reader has no tools and no memory of earlier readings. One brief, written the same way every time, carries everything: - **The project** in two lines: its name and key, and its description when it has one. - **Every record the project already holds**, by reference and title: the epics, stories, requirements, rules, use cases, RAID items and decisions, each list cut at sixty with the count of what was left out, plus the names of the terms, the personas and the stakeholders. A list the person who started the import may not read is left out rather than refused. - **The house stack**, so a brief that names another technology is answered with a decision to confirm rather than quietly built on. - **The brief itself**, between tags, up to 60,000 characters. ## How it is told to draft Every kind of record is answered with the same row (a title, three texts, a list, a category and two numbers), because a schema that changes shape per kind is one a model fills badly. The standing instructions say what each slot means for each kind: for a story the second text is the story sentence and the list is the criteria; for a rule the first text is the statement, the second the condition, the third the action and the list the worked examples; for a non-functional requirement the third text is the measure and the category the quality. A mapper gives the texts their names before anything is written, parses the story sentence into its three parts, splits "Given, When, Then" into a criterion's rows and "input → expected" into an example, and matches a category to the record's own enum by value or by label. Heimdall is told to draft from the brief and not from imagination; where it adds what any product of this kind needs, it lowers its confidence and says so in the rationale. It quotes the passage each suggestion draws on word for word, so the queue can light it up. It names a record of the project as a duplicate when a suggestion repeats one. It nests stories under their epics by key, proposes the records before the improvements, and stops at sixty rows. ## How the answer becomes the queue A row without a kind or a title is dropped, a research row is dropped (research is the sweep's kind, not the reader's), a confidence is clamped to 0 to 1, a quote is placed in the brief by searching for it, and a duplicate reference is resolved against the records the brief listed. Stories are joined to their epics by the keys Heimdall gave them. The rows are written in order as pending suggestions, and the batch is marked ready with how many it holds, which provider and model answered and the ledger row the call cost. ## Why the reading runs on the queue as you A brief takes a minute or more to read and nothing should wait on it, so the batch is queued and the page polls. The queue binds the tenant as the person who started the import: every record Heimdall checks for duplicates is read with their permissions, the call is metered to them, and a batch whose starter has left the team fails with that reason rather than reading with nobody's rights. ## What a reading costs One call to the model configured for brief imports, which thinks longer than the inline assists do, filed in the team's AI ledger under **Brief imports** and counted against the month's budget on [Settings → AI](/workspace/ai-settings). The budget is checked before the call, the usage is filed right after, and only then are the suggestions written with the ledger row they cost; a reading that fails costs nothing and leaves the batch failed with the reason. Cost grows with the brief and with the number of records the project already holds, because Heimdall reads them all to name duplicates. ## Where it stops - **It never writes a record.** A suggestion becomes a record only when a person accepts it, and then through the record's own create action, with the first version saying Heimdall drafted it and the person accepted it. - **It never decides for you.** Merge, reject and dismiss are a person's; Accept All Pending takes a kind at once, on your say-so. - **It never runs for a client guest**, who sees neither the page nor the sidebar entry. - **It never runs where assists are off**, per project, under Settings → AI. - **It never runs unmetered.** The same guard that stops any model call outside a budgeted window stops the reader. The research sweep that joins this module reads the brief's domain for what the client will expect and proposes research and improvement notes beside the records; the council then puts more than one voice on an imported scaffold before it is accepted. ## Related --- # The Council Source: https://alm-docs.labs.zuselab.dev/copilot/council By the end of this page you will know who sits on Heimdall's council, which moments it sits on, how to convene it on a record and follow the sitting in the Heimdall panel, how to accept or dismiss what the voices found, what Heimdall's verdict is, what a sitting costs and the lines the council never crosses. Heimdall is one voice, and a record needs several. The council puts named specialists beside it and lets them deliberate: each voice researches the ground from its own lens before anyone reads, reads the record and the project with the read-only tools your role allows and leaves findings, answers the other voices over the rounds of the exchange, and refines the plan Heimdall drafts from what was said; then Heimdall rules, with the plan the council agreed, the findings ranked and the disputes that remain named. Nothing reaches a record on a voice's say-so: you accept a finding or dismiss it, and a dismissed idea is never raised again. ## Before you begin | To | You need | | ------------------------------- | --------------------------------------------------------------------------------------------- | | See the council's sittings | `import:view-any` on the project, as staff; a client guest never sees the council | | Convene the council | **Use AI** (`ai:use`) on the team, and **Heimdall assists on this project** switched on | | Accept or dismiss a finding | `import:update` on the project | | Choose the voices and the share | `project:update`, under the project's [settings](/projects/project-settings#seat-the-council) | ## The voices | Voice | Lens | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Heimdall** | The moderator: dedupes what the voices said, ranks it, names where two pull apart, and never takes a side the record does not settle. | | **Odin** | Architecture and the data model: the entities the record implies, the decisions it forces, the integrations, and how it sits on the house stack. | | **Thor** | Testing: whether every statement and criterion can pass or fail, what is missing to verify it, the defects it will breed. | | **Loki** | Security: abuse cases, the data the record exposes or collects, who may see and change what, the control that closes each hole. | | **Týr** | Rigour and compliance: one statement per requirement, condition and action per rule, the authority behind each, the laws and standards the client is held to. | | **Frigg** | Planning: what the record costs to build, what it depends on and blocks, the risks to schedule and scope, what should be estimated or split. | | **Baldur** | The people who will use it: the persona's goal, the words the client reads, accessibility, error and empty states. | | **Hel** | Production: how the record fails once live, what the incident looks like, what the post-mortem would say was missing. | Every voice carries the same charge on top of its lens: quote the words it objects to, suggest in the record's own words, never repeat another voice's ground, never raise an idea the analyst dismissed, and hold the record to the house stack unless the brief rules it out. ## The moments | Moment | What the council reads | Voices by default | Convened from | | ---------------------- | ------------------------------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Intake scaffold** | A brief import on its review queue, or a project Heimdall scaffolded in conversation | Odin, Thor, Loki, Týr, Frigg, Baldur | The batch's header on the [Brief Import](/planning/brief-import#convening-the-council-on-a-batch) page, or by itself the moment a [scaffold](/copilot/start-a-project#the-council-sits-on-the-scaffold) lands in the panel | | **Requirement review** | One story, requirement or business rule, before it is approved | Odin, Thor, Loki, Týr, Baldur | The Quality panel of the record's flyout ([Quality review](/planning/quality-review#convening-the-council)) | | **Change impact** | A change request against a baseline, with the records it names and what links to them | Odin, Loki, Týr, Frigg | **Convene the Council** in the request's flyout ([Change requests](/planning/change-requests#analyse-the-impact)) | | **Release readiness** | A release before go-live | Thor, Loki, Frigg, Hel | Opens with the releases | | **Post-mortem** | An incident after the fact | Odin, Thor, Loki, Hel | Opens with the incidents | The project's [settings](/projects/project-settings#seat-the-council) choose which voices sit on each open moment and how much of the month's AI budget the council may spend on the project. ## Convening the council ### Convene it from the record On a story, a requirement or a business rule, open the flyout and find the Quality panel: **Convene the council** offers **Every seated voice** or one voice alone (**Ask Thor**, **Ask Loki**, and so on), for the question you have. On a brief import that is ready to review or reviewed, **Convene the Council** sits in the batch's header. On a change request, **Convene the Council** sits beside **Analyse with Heimdall** in the flyout, and the sitting reads under **The Council** there as well as in the panel. One sitting per record at a time; the button returns when the sitting is done. ### Follow the sitting in the Heimdall panel The Heimdall panel opens on its **Council** tab with the sitting: the moment and the record at the top with a status badge and, while the council sits, the phase it is in; who convened it and when; then the sitting as a conversation. A line says what is happening ("Summoning Thor to research testing, verification and defects…", "Loki reads the record through their lens…", "Round 2: the voices answer one another…", "Heimdall weaves the plan from what the voices said…"), and each voice's words land under its name as it speaks: what it found in its research with the sources it read, its reading of the record with its findings, its answers to the other voices by name, and its refinement of the plan. The tab polls while the council sits and refreshes itself the moment a voice finishes. **Sittings** at the top right switches between the project's sittings; the panel's tab shows the newest when nothing named one. ### Read the verdict When the voices have refined the plan, Heimdall's verdict closes the conversation: **The plan** as the council agreed it, numbered steps the analyst can act on, then **In short**, three to five sentences on what the council agrees on and what matters most, the findings worth acting on ranked with a reason each, and **Where the voices disagree** when a dispute was never settled. The draft Heimdall wrote before the refinements stays in the conversation above the voices' answers to it, so you can see what each voice changed. ### Accept or dismiss each finding Each finding carries its severity, how sure the voice was, the title, the claim, the evidence the voice quoted and what it suggests in the record's own words; a finding that drafted the missing record itself says **Drafts business rules** (or whichever kind). People who may decide the review queue see **Accept** and **Dismiss**: - **Accept** puts the finding on the review queue as a suggestion carrying the voice's name. When the voice drafted a record, the record is written there and then through its own door, in draft, with its first version stamped as Heimdall's via the council; a finding that drafted nothing is kept as a note. - **Dismiss** writes nothing and remembers the idea: no voice and no research run raises it again on this project. A decision is never undone; the finding says who decided and how. ## What the voices read Every voice reads the same brief: the project as the [research](/copilot/research) describes it (its client, the titles it holds, the ideas people dismissed), the moment, and the record under review field by field, exactly as the [quality reviewer](/copilot/quality-review) reads it; for a brief import, the brief's words and every drafted row with its decision so far; for a project Heimdall scaffolded in conversation, the whole record as the tools read it. Each voice may make a handful of read-only tool calls through the same tools the panel uses, bounded by your role, to check a rule, a term, a sibling record or an entity that holds personal data before it answers. In the research round a voice reads outward instead, with the web search beside the tools: what products of this kind do, the laws and standards that bind them where the client is, the integrations they rely on and the ways they fail, and what the house stack gives for it, and tells the council what it found with its sources. In a round of the exchange a voice reads what every voice said in the round before and the findings on the table, and answers the others by name, with at most three findings that are new or that the exchange changed. In the refinement round it reads Heimdall's draft of the plan and says, step by step, what it backs and what it would change, with at most two findings the plan still needs. A voice answers in a fixed shape: a stance in a sentence or two and a list of findings, each with a title, a claim, the evidence, a suggestion, a severity (**high** when a developer, a tester or the client would be blocked or misled, **medium** when it would be argued over, **low** for polish), a confidence from 0 to 1 and what accepting it should add: nothing, a note for the analyst, or a record it drafted in the brief import's own row shape. At most eight findings a voice in the reading, the most serious first; an empty list is a fine answer. ## Heimdall's verdict Heimdall speaks twice. Once the voices have read the record and answered one another, it reads every word in order and the findings numbered, and drafts the plan the council is converging on: five to twelve numbered steps, one line each, drawing only on what the voices said and backed, with an unsettled dispute written as the question to settle. The voices refine the draft, and Heimdall rules: the plan revised with the refinements the voices backed (a step a voice refuted that nobody defended is dropped), the summary, the ranked findings by number with a reason each (a finding that repeats another is left out and named in the kept one's reason), and the disagreements with the voices and the positions. A finding the verdict leaves out is still under its voice's words for you to decide. How far a sitting deliberates is set for the workspace by the people who run it: whether the voices research first, how many rounds of the exchange they hold (two unless set otherwise) and whether they refine the plan; a council that does not refine gets its plan with the verdict. ## What a sitting costs Every voice's turn and Heimdall's verdict are calls to the model configured for the council, which thinks longer than the inline assists do, each filed in the team's AI ledger under **Council** and counted against the month's budget on [Settings → AI](/workspace/ai-settings). The budget is checked before every call, and so is the council's share of it: the project's **Council Budget Share** (25% by default) caps what the council may spend on the project in a month, and a voice that would pass it fails with the reason while the others go on. The sitting sums its cost once concluded and shows it beside the heading. A team without a cap is never refused. A sitting is a call per voice per round plus Heimdall's two: with six voices, a research round, a reading, two rounds of the exchange and a refinement, that is thirty-two calls, and the research calls search the web as well. Convene one voice when one lens is what you need, read the latest sitting before convening another on the same record, and keep the deliberation short where the workspace allows it. ## Where it stops - **It never writes on its own.** A finding becomes a record only when a person accepts it, and then through the record's own door, attributed to the council. - **It reads what you may read.** Every tool call runs with your role's reach, so a voice sees no more of the project than you do. - **It never sits for a client guest**, who sees neither the tab nor the buttons. - **It never sits where assists are off**, per project, under Settings → AI. - **It never runs unmetered.** The same guard that stops any model call outside a budgeted window stops every voice and the moderator. ## What happens next - `council.convened` goes out when the sitting is written, `council.voice_spoke` as each voice finishes, `council.round_opened` each time a round opens (the reading after the research, each round of the exchange, the refinement), and `council.ready` or `council.failed` at the end, with the counts and the cost. - Accepting emits `council.finding_accepted` with the record written, and the review queue's own `import.suggestion_accepted`; dismissing emits `council.finding_dismissed` and `import.suggestion_rejected`. - A record a voice drafted starts its history with a version written by Heimdall via the council, and the activity log says the same. - A sitting whose voices all failed, or whose moderator could not answer, is marked **Failed** with the reason on the tab; convene it again. ## From the API and MCP Deciding a finding is a person's act: in the panel, over REST, or through `decide_council_finding` when the person tells Heimdall or an agent to; the tool pauses for their confirmation. ## Permissions | Action | Permission | | -------------------------------- | --------------------------------------- | | See the Council tab and sittings | `import:view-any`, as staff | | Convene the council | `ai:use`, with the project's assists on | | Accept or dismiss a finding | `import:update` | | Choose voices and the share | `project:update` | ## Frequently asked **Why did a voice I expected not sit?** The project's settings seat the voices per moment; a voice left unticked never sits, and **Ask Loki** on a moment Loki is not seated on is refused. Check **The Council** under the project's settings. **A voice failed. Is the sitting lost?** No. Its line says why, and the other voices and the verdict go on without it. Only a sitting where every voice failed, or whose moderator could not answer, fails as a whole. **Can I edit what a voice drafted before it is written?** Not in the Council tab: accepting writes the record as drafted, in draft status, for you to edit in its own editor with every change kept as a version. Dismiss it if the draft is wrong at heart. **Why does the verdict rank fewer findings than the voices left?** Heimdall leaves out a finding that repeats another and names the kept one, and one that does not hold against the record. The left-out findings stay under their voices for you to decide. ## Related - [Quality review](/planning/quality-review) — Heimdall alone on one record, and where the council is convened from - [Brief import](/planning/brief-import) — the intake scaffold the council sits on, and the review queue accepted findings join - [Research](/copilot/research) — the project brief every voice starts from - [Project settings](/projects/project-settings#seat-the-council) — the voices per moment and the council's share of the budget - [Impact analysis](/copilot/impact-analysis) — the brief the change impact moment reads, and Heimdall's own reading of it - [Status reference](/reference/status-reference#council-session-status) — where a sitting and a finding stand - [Usage and budgets](/copilot/usage-and-budgets) — where every call is filed - [Safety and permissions](/copilot/safety-and-permissions) — the rules every Heimdall feature keeps --- # Impact analysis Source: https://alm-docs.labs.zuselab.dev/copilot/impact-analysis By the end of this page you will know how Heimdall's impact analyst thinks: what it is given, what it weighs, how it must answer, what of its answer is written onto the change request and what is not, what a reading costs, how the council's change impact moment reads the same brief through several lenses, and the lines it never crosses. The task itself, pressing **Analyse with Heimdall** and keeping or changing the numbers, is on the [change requests](/planning/change-requests#analyse-the-impact) page of the planning guide. ## What Heimdall is given The analyst has no tools and no memory of earlier turns. Everything it knows arrives in one brief, written the same way every time and read through the same services a person reads through, so it sees nothing the person who asked could not open: - **The request as it was raised**: its reference, title, reason, priority and status, who raised it and the stakeholder it speaks for, and what is asked for. - **The impact the analyst already wrote**: the scope and the risk as prose, and the numbers (the level, the schedule in days, the effort in hours), or _none yet_ when nothing was written. - **The baseline it changes**: its name, kind, status and whether the client signed it, and every record it holds by key, title and version, unlocked ones marked, so a record the request forgot can be named. A request against no baseline is weighed against the record as it stands. - **Every record the request names**, in full: a story, a requirement or a rule as the quality reviewer describes it (its fields, the project's business rules, its siblings), an epic or a use case by its key, title and status, and under each **what links to it** from either side: the stories that implement a requirement, the tests that verify them, the rules and the data they lean on. ## What it is told to look for The analyst is briefed for the business analyst and the project manager who will decide the request, and told never to decide it and never to invent a fact about the product: - **Scope**: every record the change touches, the named ones and the forgotten ones, written as short HTML naming records by their reference; for a scope reduction, what is removed and what depended on it. - **Cost**: the effort in hours for the house stack, analysis to release with no padding, and the days the schedule moves, negative when the change shortens it; the summary says what the estimate assumes. - **Risk**: regressions in the records that link to the ones changed, security and data exposure, the client's expectations after a signed baseline, the dates already promised, and what the request leaves ambiguous. - **Level**: low when absorbed with no date or price moving, medium when a date or the effort moves but the plan is reworked rather than renegotiated, high when the scope, the schedule or the budget needs the client's agreement again. - **Questions**: at most five to put to the client or the team before the request goes to the approvers; none when the request is clear. Where the analyst already wrote an impact, Heimdall weighs it and says where it differs rather than restating it. ## How it must answer The answer is structured, not prose: a summary of two or three sentences, the scope and the risk as HTML, the schedule in days, the effort in hours, the level as one of the three words, the records touched as a list of reference, title and how, and the questions as a list. A row without a reference is dropped, an empty question is dropped, an unknown level falls back to medium, an effort below zero to zero, and every text is cut to the length the request's fields hold. ## What is written and what is not The whole answer is kept once, as **Heimdall's reading** on the request: the summary, the scope, the risk, the records touched and the questions, with when it was read. The request's own fields are filled from it only where they were empty: the level, the schedule, the effort, the scope and the risk. A number or a text the analyst wrote is never overwritten, so a second reading changes the draft and leaves the fields alone. A request still only submitted moves under analysis, because it has now been analysed. The save is a [version](/planning/revisions-and-versions) of the request with the reason _drafted by Heimdall_, and `change_request.analysed` goes out with the level and how many records the reading named. Heimdall estimates; the analyst decides. Edit the request after a reading and the fields are yours; ask again and Heimdall weighs what you wrote. ## What a reading costs Each reading is one metered call under the team's [AI budget](/copilot/usage-and-budgets), filed as **Impact analysis** with the project, the person who asked and the model's tokens; the budget is asked for room before the model is called, and a team past its cap gets a refusal, not a reading. The impact model is chosen by the workspace's AI settings; it thinks before it answers. ## The council on a change For a change that deserves more than one lens, the [council](/copilot/council) sits on its **Change impact** moment: Odin for the architecture and the data, Loki for the security and the exposure, Týr for the rigour and the client's agreement, Frigg for the schedule and the capacity, by default. Each voice reads the same brief the analyst reads, with the read-only tools the person who convened it may use, and leaves findings; Heimdall's verdict ranks them and names where the voices disagree. A finding that names a record the request forgot is worth more to the council than a restatement of the request's words. The sitting is convened from the request's flyout and reads under **The Council** there; its cost is the council's share of the budget, one call per voice plus the verdict. ## Where it stops - It never decides a request, never submits it to the approvers and never unlocks a record; the approval engine does that on the people's word. - It reads through the person's own visibility: a record they may not open is named as hidden, and a client guest cannot ask for a reading at all. - It runs only for staff with `change-request:analyse` and `ai:use`, on a project whose assists are on, within the team's budget. - A provider that does not answer leaves the request untouched and files nothing; the message says Heimdall was unavailable. ## Related - [Change requests](/planning/change-requests) — raising, analysing, approving and seeing a change through - [Baselines](/planning/baselines) — the frozen record a change request unlocks - [The council](/copilot/council) — the voices, the moments and the verdict - [Quality review](/copilot/quality-review) — the sibling reading of one record before it is approved - [Usage and budgets](/copilot/usage-and-budgets) — where every reading is filed - [analyse_change_impact](/mcp/v1/tools/planning/analyse-change-impact) — the same reading on an agent's word --- # Improvement Sweep Source: https://alm-docs.labs.zuselab.dev/copilot/improvement-sweep By the end of this page you will know what the improvement sweep checks, where its cards appear, how to run it now and when it runs on its own, what keeping or dismissing a card does, and why it costs nothing. A planning record grows by accretion: a requirement written in a hurry has no measure, a story has criteria for the happy path and none for the refusal, a rule has no worked example, an entity has no owner, a risk sits on the log with no mitigation. None of these stops a sprint, and all of them cost a conversation later. Heimdall's sweep reads the record for exactly these gaps and leaves one card per record on the review queue, so the analyst fills them when there is a quiet moment rather than when a developer asks. ## What the sweep checks | Check | Flags | Skips | | ---------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------- | | **Measure** | A non-functional requirement whose measure is empty | Functional requirements; deprecated requirements | | **Negative criterion** | A story with no negative acceptance criterion, whether it has other criteria or none | Tasks, bugs and spikes; done or cancelled stories | | **Worked example** | A business rule whose examples are empty | Deprecated rules | | **Owner** | A data entity with no owner | Deprecated entities | | **Mitigation** | A risk that is open, monitoring or mitigating with no mitigation | Assumptions, issues and dependencies; closed risks | Each gap becomes an **Improvement** card titled with the record's reference, such as _Add a measure to CRM-NFR-3_, with a sentence on what to add and why, a one-line reason, and **Open CRM-NFR-3**, which opens the record's page with its flyout showing. The cards sit under **Waiting for you** on the [Heimdall suggests rail](/projects/overview#ask-heimdall-about-the-domain) of the project's overview, beside what the [research](/copilot/research) left. ## Running it **Check the record** on the Heimdall suggests rail runs the sweep now and answers with a toast: how many gaps it found, or that nothing is missing. People who may start a brief import see it, on a project whose assists are on. The sweep also runs on its own: - **Weekly**, on every live project whose assists are on, early on Monday. - **After a brief import is fully decided**, since the records it just wrote are where the gaps usually are. A sweep that finds nothing new still runs the retirement below, so the rail stays honest. ## Why nothing is proposed twice The card's title carries the record's reference, so the same gap on the same record has the same title every time. Before it proposes, the sweep reads every card the project already holds on the review queue and every idea people dismissed, and skips a gap whose title is among them: a card you **kept** stays kept, a card you **dismissed** is never raised again, a card still **waiting** is not doubled. A waiting card whose gap was filled since, because someone added the measure or the example, is taken down on the next sweep; a kept or dismissed card is left as a record of the decision. ## Keeping and dismissing The cards are suggestions like any other on the rail: **Keep** holds the note on the project for everyone to read, **Dismiss** drops it and remembers it. Neither writes to the record; the record is changed in its own editor, and the next sweep notices. Fill the gap first and the card leaves on its own; keep the card when the gap is meant to stay for now and you want it on the record; dismiss it when the check does not apply, such as a risk the client accepted as it stands. ## What it costs Nothing. The sweep reads the record and calls no model, so no usage is filed and the team's AI budget does not move. It is switched off with the project's assists, like every other suggestion. The sweep is the deterministic half of Heimdall's standing suggestions. The judgement calls, whether a criterion can be verified or a rule contradicts another, stay with the [quality review](/planning/quality-review) and the [council](/copilot/council), which read with a model and are metered. ## What happens next - `improvement.swept` goes out with how many cards were proposed, how many were taken down and how many wait, so a dashboard can follow how complete a project's record is. - Keeping or dismissing a card emits `import.suggestion_accepted` or `import.suggestion_rejected`, as on every suggestion. - Nobody is notified; the rail is where the cards wait. ## From the API and MCP ## Permissions | Action | Permission | | ---------------------- | ---------------------------------------------- | | See the cards | `import:view-any`, as staff | | Run the sweep now | `import:create`, with the project's assists on | | Keep or dismiss a card | `import:update` | ## Frequently asked **I fixed the record, but the card is still there.** The card leaves on the next sweep: press **Check the record**, or wait for the weekly one. Keeping or dismissing it also closes it, but a dismissal is remembered and the same gap would not be raised again if the record regressed. **Can I turn one check off?** Not per check. Dismiss the cards a check raises where it does not apply; the dismissals are remembered per record, so the check stays useful for the rest of the project. **Why did the sweep find nothing on a project with obvious gaps?** It reads only the five shapes above. A vague criterion or a rule that contradicts another is not a structural gap; the [quality review](/planning/quality-review) reads for those. ## Related - [The project overview](/projects/overview#ask-heimdall-about-the-domain) — the rail the cards wait on - [Research](/copilot/research) — the other cards on the rail, from the web - [Quality review](/planning/quality-review) — the metered reading of one record - [Brief import](/planning/brief-import) — the review queue, and the decisions the cards share - [Usage and budgets](/copilot/usage-and-budgets) — what is metered and what is not --- # Heimdall Source: https://alm-docs.labs.zuselab.dev/copilot **Heimdall** is an operator that works inside your workspace. You tell it what you want in your own words, "which projects are in development?", "pause the CRM sync endpoint", "what changed on my projects this week?", and it answers from your live record or does the work, through the same operations the pages, the REST API and the MCP server use. It runs as you, with your permissions, and anything that changes or removes what already exists waits for your confirmation. Every answer is costed at the model's list price and summed against your team's monthly AI budget. The line under the composer says where the month stands, and at the cap Heimdall stops rather than running over. [Usage & Budgets](/copilot/usage-and-budgets) has the meter. ## Where to find it A sheet that slides in beside whatever page you are on and stays open while you move around. Press **Ask Heimdall** in the sidebar or Ctrl+/. **Ask Heimdall** beside any long field of a planning record: draft it, improve it, shorten it, make it testable or rewrite it as Given, When, Then, then take the draft or drop it. **Review now** on a story, a requirement or a rule: Heimdall reads it the way a developer and a tester will, scores it out of 100 and lists what to fix, quoting the words and suggesting the rewrite. The month's standing, the statement per feature and model, the latest calls, and the cap and warning line the team owner sets. Paste a client's brief: Heimdall drafts epics with their stories, requirements, rules, terms, personas, stakeholders, risks and decisions, quotes the passage each came from, and you accept, edit, merge or reject each one from a review queue. **Research the domain** on a project's overview: Heimdall reads the web for what this kind of product needs, the rules that apply and the integrations the client will expect, writes notes with their sources and leaves suggestions you keep or dismiss. **Convene the council** on a story, a requirement, a rule or a brief import: Odin, Thor, Loki, Týr, Frigg, Baldur and Hel each read it through their own lens, Heimdall writes the verdict, and you accept or dismiss what they found. **Check the record** on a project's overview, and every week on its own: Heimdall reads the record for a requirement without a measure, a story without a negative criterion, a rule without an example, an entity without an owner and a risk without a mitigation, and leaves a card per gap. No model, no cost. ## What it does for you Which projects are live, who leads each, what changed this week, which webhook endpoints are failing, who is on the team and what their role allows. It calls the right reads and answers with the facts. A webhook endpoint registered, a delivery retried, every notification marked read: made at once, with a button to open the page it touched. Pausing an endpoint, rotating its secret, deleting it, revoking a token: prepared, shown with its exact arguments, run when you confirm. When the record it reads is thin it says what is missing and offers to draft it, and every technical proposal assumes the stack Zuse Technologies builds on unless the brief rules it out. It holds exactly the permissions you hold. For a team member, Heimdall is bounded by their role, and every change is written to the activity log as Heimdall's, on your behalf. Every operation Heimdall runs is a tool on the [MCP server](/mcp/v1) too, so what it can reach grows with the catalogue, module by module. The tool families land module by module, with the modules themselves: today Heimdall reaches your account, the project registry, the teams and their members, roles and groups, API tokens, webhook endpoints and deliveries, notifications and the activity log. When no tool covers what you ask, it says so and names the page that does. ## A turn, step by step **You ask.** Type in the composer, or tap one of the questions an empty thread offers. Write the way you would to a colleague who knows your projects. **It works in the open.** Between your message and the answer a timeline grows as it happens: the model's thinking as a faded line you can unfold, every tool as a step with the arguments it used hanging off it and the time it took, and "Working for 3s · 2 steps" counting above. Once the answer is in, it folds to one line, "Worked for 4.1s · 3 steps", and opens with a tap. **The answer streams in**, formatted as it arrives, with a button for anything it made or read that has a page, and two or three follow-up questions you can send with one tap. **A card when it matters.** If the work changes or removes what exists, it stops first: a card names the action and its arguments and waits for **Confirm** or **Cancel**. Confirming runs it; cancelling costs nothing. *Heimdall · Overview* **You:** Which webhook endpoints are failing? Worked for 2.8s · 3 steps - Thought: A failing endpoint shows as dead-lettered deliveries. The delivery log filtered to dead answers it, and the endpoint list names each one. - List webhook deliveries (0.4s): status · dead, limit · 25 - List webhook endpoints (0.3s): limit · 25 **Heimdall:** One endpoint is failing. **CRM sync** has three dead-lettered deliveries since Tuesday, all `project.updated`, each refused with a 503 from the receiving host. **Slack digest** and **Deploy board** are delivering normally. Buttons: ↗ Open the deliveries · 💬 Retry the dead deliveries · 💬 Pause CRM sync · 💬 Who registered it? Nothing changed here: reads run at once and show no card. Tap **Retry the dead deliveries** and the next turn replays them, which adds to the record and runs at once too; tap **Pause CRM sync** and you get a card first. ## Heimdall or MCP Zuse Bifrost is agent-native: every operation Heimdall runs is also a tool on the [MCP server](/mcp/v1), so Claude, ChatGPT, Cursor or an automation of your own can run your workspace too. Both roads lead to the same operations under the same permissions. They differ in who runs the model, where the conversation lives and what else is in the room. | | Heimdall | Your own agent over MCP | | ---------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | Where it lives | Beside every page of the app | Wherever you already work: Claude, ChatGPT, Cursor, a pipeline, code of your own | | Setup | None. Press **Ask Heimdall** | Connect once over OAuth from the assistant's connector settings, or mint a token | | The model | Chosen and run by Zuse Bifrost, with a router that picks a lighter or a stronger model per message, or the tier you pick | Whichever your agent runs, with its own memory, files and other tools beside Zuse Bifrost | | What it costs | Model usage at list price, against the team's monthly AI budget | Nothing from Zuse Bifrost; your assistant's own plan or API bill | | What it reaches | Your projects, teams, webhooks, tokens and history, as you | The same, bounded by the token's abilities and project scope | | Before a change | A card with the exact arguments; **Review every change** asks for new things too | Your agent's own approval rules; every tool is marked read-only, additive or destructive, so a good client asks before the destructive ones | | The conversation | Kept in Zuse Bifrost for ninety days with every step, button and card | Kept by your agent | | The record | Every change in the activity log as Heimdall's, on your behalf | Every change in the activity log as the token's | The two are not exclusive. A team can press **Ask Heimdall** in the morning and run the same operations from Claude Code in the afternoon, and the activity log tells the two apart. ## Where it is going The operator is the first of Heimdall's surfaces. Each of the next ones arrives with the module it serves and with its own page here: inline assists on every editor, the brief import that drafts a project's planning scaffold from a paragraph, quality reviews on stories and requirements, the [impact analysis](/copilot/impact-analysis) of a change request, research into a project's domain, the opening page of a [generated document](/documents/generated-documents), and, as their modules land, test generation, release notes, incident helpers, ticket triage and the Monday digest. All of them run through the same budget and the same ledger. ## Questions No. It runs only while you are talking to it and only as you. Nothing is scheduled, nothing runs in the background, and anything that changes what already exists waits for your confirmation on a card. No. It sees the team you act in and nothing else, through the same permission checks the REST API and the MCP server apply. A guest of one project cannot open Heimdall at all. Whatever you write in. The panel and the cards are in English; the answer itself follows the language of your message. The team owner always can. A team member's role has to carry **Use AI**, and Heimdall then holds exactly their role's permissions. Their turns draw on the team's budget. [Permissions & Safety](/copilot/safety-and-permissions) has the detail. It stays in the thread list for ninety days after its last turn and is then removed. Open it again before that and the whole conversation is there, with each answer's work timeline, its buttons and its cards. ## Related - [The panel](/copilot/panel) — the sheet beside every page, end to end - [Usage & Budgets](/copilot/usage-and-budgets) — what a turn costs and how the month is capped - [Permissions & Safety](/copilot/safety-and-permissions) — who may use it and what the model is shown - [Settings → AI](/workspace/ai-settings) — the budget, the statement and the models - [MCP server](/mcp/v1) — the same operations for an agent of your own --- # Inline assists Source: https://alm-docs.labs.zuselab.dev/copilot/inline-assist By the end of this page you will have asked Heimdall to draft an empty field from a title, to sharpen a paragraph you wrote in a hurry, and to turn a requirement into steps a tester can tick, and you will know what Heimdall sees when it does, what it costs, and why nothing changes until you save. An **inline assist** is one bounded rewrite of one field. Beside every long field of a planning record's editor, **Ask Heimdall** opens a short menu; pick an assist and the draft appears under the field, with a line on what Heimdall assumed or left open. **Replace** puts the draft in the field's place, **Add Below** keeps your words and adds the draft after them, **Discard** drops it. Nothing reaches the record until you save the form as you always do. The fields that offer it are the rich-text ones: an epic's summary, value and measures; a story's description and a bug's steps and results; a requirement's statement and rationale; a rule's statement and exceptions; a use case's conditions; a RAID item's description, mitigation and contingency; a decision's context, decision and consequences; a data entity's description; a glossary definition. ## Before you begin | You need | Where it comes from | | -------------------------- | ----------------------------------------------------------------------------------------------------------- | | `ai:use` | The permission that opens Heimdall; the staff roles have it, a client guest never does. | | Heimdall assists on | The project setting **Heimdall assists** in [project settings](/projects/project-settings), on by default. | | Room in the month's budget | The team's AI budget under [Settings → AI](/workspace/ai-settings); an exhausted budget refuses the assist. | ## Ask for a draft [#ask] 1. Open a record for editing, or press **New** on its page. 2. Under the field, press **Ask Heimdall** and pick an assist. **Draft it** works on an empty field from the record's title; the other four need words in the field to work on. 3. Wait a few seconds. The draft appears under the field with a **Heimdall notes** line when there is something you should know. ## Take it or drop it [#take] - **Replace** puts the draft in the field. Read it once more: Heimdall keeps every fact and reference it was given, but it cannot know what you left out. - **Add Below** keeps your words and adds the draft after them, for a second scenario or an alternative you want to compare. - **Discard** drops it; asking again gives a fresh draft. Then save the form. Because a draft was taken into it, the version written is marked **Drafted by Heimdall**: Heimdall as the author of the words, you as the person who accepted them, and the activity row saying it came through an inline assist. A save with no draft taken is your own version, as before; opening another record forgets any draft you took and did not save. ## The five assists | Assist | What Heimdall does | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | **Draft it** | Writes a first version from the record's title, the facts it was given and the project's purpose; names every assumption in the notes. | | **Improve it** | Keeps the meaning and sharpens the words: plain, specific, one idea per sentence, every fact and reference kept. | | **Shorten it** | Says the same in roughly half the words, dropping repetition and hedging, never substance. | | **Make it testable** | Replaces vague words with measurable ones and splits compound statements; an unknown measure becomes a placeholder named in the notes. | | **Given, When, Then** | Rewrites the words as acceptance scenarios, each a short title with Given, When and Then lines, inventing no behaviour. | ## What Heimdall sees Only what the assist needs: the field's label and current words, what the record is and its title, the project's name, and a few facts the page adds (a decision's kind, for instance). It does not read the rest of the form, the record's history, other records or your conversation in the panel, and it holds no memory from one assist to the next. Every assist carries the house stack (Laravel and Livewire) so examples match how the team builds. ## What it costs Each assist is one model call, metered like a turn in the panel: the budget is checked before the call and the usage filed after it under the **Inline assist** feature of the [usage statement](/copilot/usage-and-budgets). When the team crosses its warning line the owners are told; when the cap is reached the assist is refused until the month turns or the cap is raised. ## Safety - Heimdall writes nothing to the record. The draft lives under the field until you replace, add or discard it, and only your save writes a version; that version says Heimdall drafted it and you accepted it, so the record's history never hides where the words came from. - The draft is sanitised to plain paragraphs, lists and emphasis before it is shown; a model cannot smuggle markup or links into a record. - The call runs inside the same metering gate as every other model call in Zuse Bifrost: without the budget check and the usage line there is no way to the model. - A provider that does not answer is reported as unavailable; nothing is filed and nothing changes. ## Permissions | Action | Permission | | -------------------- | ------------------------------------------------------------------------------------- | | See Ask Heimdall | `ai:use`, on a project whose **Heimdall assists** setting is on; never a client guest | | Take or drop a draft | The same; saving the record then needs its own update permission | ## Frequently asked Either your role lacks `ai:use`, the project's **Heimdall assists** setting is off, or you are reading the record as a client guest. Only rich-text fields offer it; titles and short fields do not. Not from the field. An assist rewrites one field; drafts that create records, such as stories from a use case's flows or requirements from a brief, land in a review queue so each one is accepted on its own. That queue is the brief import, which is on its way. Yes, twice. The usage statement shows the assist, and the version your save writes is marked **Drafted by Heimdall** with you as the person who accepted it, the same way a record accepted from a brief import is. A version written directly by Heimdall through the panel is marked as its own, with the thread it came from. ## Related - [The panel](/copilot/panel) — Heimdall in conversation, with tools - [Usage and budgets](/copilot/usage-and-budgets) — where each assist shows up - [Safety and permissions](/copilot/safety-and-permissions) — what Heimdall may and may not do - [Project settings](/projects/project-settings) — the Heimdall assists switch --- # The Panel Source: https://alm-docs.labs.zuselab.dev/copilot/panel Heimdall panel is a sheet that slides in from the right of any page. The page keeps its width and stays readable behind it, and because the panel is mounted once in the layout it keeps your thread open while you move between pages. ## Your first turn **Open it.** Press **Ask Heimdall** in the sidebar, under the search, or press Ctrl+/ (⌘+/ on a Mac). The shortcut is printed beside the sidebar entry, the way the search shows its own. **Check the chip.** Under the header a chip names what Heimdall is looking at: the project whose page you are on, or **Overview** when you are on none. That context travels with every message. **Ask.** Type in the composer and press Enter, or tap one of the seven questions the start screen offers. Shift+Enter{" "} breaks a line. The small **Auto** button beside Send picks the model tier and switches review on. **Watch it work.** A timeline grows between your message and the answer: the thinking as a faded line, every tool as a step with its arguments and the time it took, "Working for 3s · 2 steps" counting above; then the answer arrives word by word, formatted. A step that failed says why under its arguments: the refusal a tool answered with, or that the run reached its step limit before the call, in which case Heimdall carries on and runs it in the next step. **Act on the answer.** Press a button to open what it made or read, tap a follow-up to send the next question, or **Confirm** a card when it asks before changing something. ## The panel, piece by piece Heimdall's mark and name, the **Chat** and **Council** tabs, then one group of buttons: the **Threads** button that lists your recent conversations with **New thread** at the top, the thread's menu while a thread is open, **Widen** (the panel grows to the sidebar's edge and the conversation keeps a reading column; **Narrow** brings it back, and the browser remembers the choice), and the close button. Esc closes it too. Two tabs beside the name. **Chat** is the conversation. **Council** shows the sittings of [Heimdall's council](/copilot/council) on the project in view, the newest first, with a card per voice and the verdict pinned on top, and lets the people who may decide accept or dismiss each finding. The tab follows the project you move to, and a record's page opens it on the sitting it just convened. The project or page in view, and, while the eye beside the chip is lit, the text of the page itself, read at the moment you send: what is on screen first, the rest of the page after it, up to 8,000 characters. So "why is this one amber?" or "add up this column" works on any page, and "pause the endpoint I have open" needs no name. Tap the eye to keep Heimdall off a sensitive page; your browser remembers the choice. What it reads counts toward the turn's tokens like the rest of the prompt. An empty thread shows seven day-to-day questions, each a quick read that changes nothing: which projects are in development, what changed this week, who leads each project, your unread notifications, which endpoints are failing, who is on your team, which tokens you hold. Tap one and it is sent as if you had typed it. Your messages on the right, Heimdall's under its mark, each answer's work folded to one line above it ("Worked for 4.1s · 3 steps"), the buttons, the follow-ups and the cards. Under every answer a small line says what it used: the tier and the model that answered, the prompt tokens with the cached part, the output tokens, the time, and what the answer cost. A table in an answer is shown as a card per row. One line that grows as you type, up to six, with the model button and the send button beside it. A message may be up to 4,000 characters, in any language. Under the composer: "AI budget this month: $12.40 of $100.00 (12%)", or "AI spend this month: $12.40, no cap set" for a team without a cap. At the cap it reads "This month's AI budget is spent", with a **Raise it** link for a person who may. ### Keyboard | Keys | Does | | --------------------------------- | ------------------------------------ | | Ctrl+/ | Opens the panel, and closes it again | | Esc | Closes the panel | | Enter | Sends the message | | Shift+Enter | Breaks a line inside the message | | Keys | Does | | ---------------------------------- | ------------------------------------ | | ⌘+/ | Opens the panel, and closes it again | | Esc | Closes the panel | | Return | Sends the message | | Shift+Return | Breaks a line inside the message | ## Who sees the entry **Ask Heimdall** is shown only to people who may use Heimdall: the team owner and any team member whose role carries **Use AI**. A member whose role lacks it sees "Heimdall is not in your role" instead of the composer, and nothing is metered ([Permissions & Safety](/copilot/safety-and-permissions)). A guest of one project never sees it. ## Threads The **Threads** button in the header lists your recent threads, newest first, each named after its first message, with **New thread** at the top. Open an old one and the whole conversation is there, with each answer's work timeline, its buttons and its cards. A thread nobody has used for ninety days is removed. A thread opened from a project page stays about that project for its whole life; a thread opened elsewhere takes the project in view turn by turn. When no project is in view and the request needs one, Heimdall lists your projects and asks which, unless you have only one. While a thread is open, the **⋯** menu beside **Threads** offers two more things: | Action | What happens | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Archive thread** | The thread leaves the list and nothing continues it. It keeps its conversation and sits under **Archived threads** in the switcher, where one tap restores and opens it. | | **Delete thread permanently** | After you confirm, the thread goes for good with its conversation, its tool runs and its cards. There is no undo. | A message whose turn could not run, because the model was unreachable or the budget was spent, leaves no thread behind: the notice appears under the conversation, your words stay in the composer, and the switcher stays as it was. ## Model and review The **Auto** button beside Send opens the model menu. Auto, the default, lets Heimdall pick the model for each request: a small router model reads the message and sends a one-lookup question to the simple model and anything that writes, drafts, compares or takes several steps to the involved one. The three named tiers pin a model, and the dots beside each say what it costs relative to the others. | Tier | Best for | Cost | | -------- | ------------------------------------------ | ------------- | | **Auto** | Everyday use; the router picks per request | Standard cost | | **Lite** | Quick reads and simple edits | Lowest cost | | **Pro** | Sharper answers and multi-step work | Standard cost | | **Max** | Deepest reasoning for the hardest requests | Highest cost | Under the tiers, **Review every change** makes every tool that writes wait for a card, the additive ones included, so a new webhook endpoint is shown to you before it exists. With review off, only changes to what already exists wait. A thread remembers the tier and the switch you last used on it. ## While it works Between your message and the answer a timeline grows as the turn runs: the model's thinking as a faded line with a hollow dot, every tool as a step with the arguments it used hanging off it on a braid of lines and the time it took at the right, and "Working for 3s · 2 steps" counting above. A running step shows a hollow dot too. Under the header a line in Heimdall's own voice says which phase the turn is in: "Bifrost firing up…" while the turn is routed, "Researching the projects across the nine realms…" while it reads, "Forging an epic in Asgard…" while it writes, "Heimdall speaks…" while the answer streams. When the answer is in, the work folds to one line, "Worked for 4.1s · 3 steps". Tap it to open the rail again, tap a thought to read it in full; a failed tool shows a red dot with "Failed" under it, a cancelled card a grey one. Streamed word by word and rendered as it arrives: bold, lists and short tables read correctly even mid-sentence. ## After the answer When a turn made or read something that has a page, a purple button opens it: **View CRM sync** for the endpoint it just paused, **Open the deliveries** for the log it worked through, **Open the Notifications Center** after it marked everything read. The button navigates without leaving the thread. Under the latest answer, two or three secondary buttons with the requests you are likely to make next, phrased as you would type them ("Retry the dead deliveries", "Who leads it?"). Tap one to send it. While Heimdall writes records the page follows it: when it creates epics the Epics page opens behind the panel and the table fills in row by row as each one lands, the sidebar's counts climb with them, and a band of light runs round the edge of the window until the turn ends. A turn that changed something else refreshes the page behind the panel, so an endpoint it paused shows as paused in the table beside you. ## What it wrote When a turn creates or changes a record, a card appears under the answer: the kind (an epic, a story, a client, a glossary term), its reference where it has one, its title and where it stands, with **Open** to the record's page. A turn that wrote more than a handful folds the records of one kind into one card ("Components · 5 new", the first three names and how many more) that opens the page they live on, and shows four cards with **Show N more** for the rest. The cards read from what the tools answered, never from the prose, so a record the model mentions but did not write gets no card. A turn that convenes the [council](/copilot/council) shows the sitting itself in the thread as the conversation it is: each voice's research, reading, answers and refinements as they land, Heimdall's plan and verdict at the end, and each finding carries **Accept** and **Dismiss** for the people who may decide, the same as on the Council tab. Start a project this way: tell Heimdall about the client and what they need, in a paragraph. It creates the client and the project, writes the components, environments, people, epics, stories, requirements, rules and glossary as drafts, asks only for what it cannot infer, and convenes the council on the result; the walkthrough is on [starting a project in conversation](/copilot/start-a-project). ## Confirmation cards Anything that changes or removes what already exists stops before it runs; creating something new does not, unless **Review every change** is on. An amber card shows **Confirm before I do this**, the operation in words, every argument it will use, and two buttons. *Heimdall · Settings · Webhooks* **You:** Pause the CRM sync endpoint. Worked for 2.1s · 2 steps - List webhook endpoints (0.3s): search · CRM sync - Pause webhook endpoint: endpoint_id · CRM sync Card: Confirm before I do this · Pause webhook endpoint · endpoint_id: CRM sync · Confirming counts as a turn; cancelling is free. · [Cancel] [Confirm] **Confirm** runs it and Heimdall finishes its answer; **Cancel** declines it, and nothing happens. A card expires after ten minutes; ask again and Heimdall prepares the action afresh. Confirming is a model call of its own, because Heimdall reads the result and writes its answer, so it is metered like a message; cancelling costs nothing. Heimdall never splits a destructive action into smaller pieces to avoid the card, and never repeats an action you cancelled. A tool that refuses because your role does not allow it is reported plainly; it does not look for another way round. ## The budget line, state by state | The line reads | Meaning | | ---------------------------------------------- | -------------------------------------------------------------------------------- | | AI budget this month: $12.40 of $100.00 (12%) | The team's month against its cap; every turn and every other AI feature moves it | | AI spend this month: $12.40, no cap set | The team chose no cap; the spend is shown for the record | | This month's AI budget is spent · **Raise it** | The composer refuses to send until the cap is raised or the month turns | [Usage & Budgets](/copilot/usage-and-budgets) has the whole meter. ## When something goes wrong | You see | What it means | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | | "Heimdall could not reach its model just now" | The provider did not answer; nothing was changed or metered. Try again in a moment. | | "This answer failed before it finished. Nothing was filed for it." | The turn broke mid-way; the thread keeps the note and nothing is filed in the ledger | | A red dot on a step of the work, "Failed" under it | That tool failed; Heimdall says what it could not do | | "Heimdall is not in your role" | Your role lacks **Use AI**; ask the team owner to add it | | "This month's AI budget is spent" | The cap is reached; raise it under Settings → AI or wait for the next month | ## Questions Yes. It is mounted once in the layout, so it survives navigation with its thread open, its scroll position and the text you were typing. Yes. Name it ("who leads the CRM project?") or let Heimdall ask which one. A thread started from a project page keeps that project for its whole life. Ninety days after its last turn. The **Threads** button lists the recent ones; anything older is removed on its own. Up to 4,000 characters. The composer grows to six lines and scrolls beyond that. Because the question needed none: a definition, a how-to, or something it already read earlier in the thread. Anything about your record is answered from a tool; it never guesses a figure, a date or an id. ## Related - [Usage & Budgets](/copilot/usage-and-budgets) — the meter behind the budget line - [Permissions & Safety](/copilot/safety-and-permissions) — who may use it and what the model is shown - [Settings → AI](/workspace/ai-settings) — the cap, the statement and the models - [MCP tools](/mcp/v1/tools-reference) — every operation Heimdall can run, under its kind --- # Quality review Source: https://alm-docs.labs.zuselab.dev/copilot/quality-review By the end of this page you will know how Heimdall's quality reviewer thinks: what it is given, what it is told to look for, how it must answer, how its answer becomes a score, and the lines it never crosses. The task itself, pressing **Review now** and working through the findings, is on the [Quality review](/planning/quality-review) page of the planning guide. ## What Heimdall is given The reviewer has no tools and no memory of earlier turns. Everything it knows about the record arrives in one brief, written the same way every time: - **The record, field by field.** A story's sentence, description, kind, status, priority, points, labels, epic and persona, and every acceptance criterion with its Given, When and Then; a requirement's statement, rationale, measure, verification method and source; a rule's statement, condition, action, exceptions, worked examples, authority and dates in force. An empty field is named as empty, so a missing rationale is a fact Heimdall sees, not a gap it has to guess. - **What the record is linked to**, read from the record's side: the requirement a story implements, the rule a requirement honours. - **The project's business rules**, each by reference, name and statement, so a conflict is named against a real rule rather than a hunch. - **The titles of the record's siblings**: the other stories of the same kind, the other requirements of the same type, or every other rule in full, so a duplicate is named by reference. - **The house stack**, so a record that assumes another framework is caught before a developer reads it. Markup is stripped and long fields are cut, and anything the person asking may not read (a rule list on a project where their role cannot see rules) is left out rather than refused. ## What it is told to look for The brief carries the same standing instructions on every review: the INVEST qualities for a story, outcome and done-ness for a task or a bug, one testable statement with a rationale and a measure for a requirement, condition and action and examples and authority for a rule; vague words to flag; conflicts and duplicates to name by reference; and the house stack to hold the record to. The full checklist per kind, with the finding codes, is on the [planning page](/planning/quality-review#what-heimdall-checks). Heimdall is told to quote the words it objects to, to suggest a rewrite in the record's own words, to report nothing when the record is sound, never to report the same problem twice, and to stop at ten findings with the most serious first. ## How it must answer The reviewer answers in a fixed shape, not prose: a short summary and a list of findings, each with a code from the fixed list, a severity of high, medium or low, a message, a suggestion and the field concerned. A finding that comes back without a message is dropped; a code or a severity the list does not know falls back to **Other** and **Medium** rather than breaking the panel. The model never names a score. ## Why the score is arithmetic A number from a model is a guess that changes between two runs on the same text. The score is instead 100 less the penalties of the findings Heimdall graded (25 for high, 10 for medium, 4 for low), floored at 0, so two reviews with the same findings score the same and a person can read why a record scored 61 from the cards alone. The band is a reading of the score: **Ready** from 80, **Needs work** from 50, **Not ready** below. ## What a review costs One call to the model configured for quality reviews, which thinks longer than the inline assists do, filed in the team's AI ledger under **Quality reviews** and counted against the month's budget on [Settings → AI](/workspace/ai-settings). The budget is checked before the call, the usage is filed right after, and only then is the review written with the ledger row it cost, so a review never exists without its cost and a failed call never costs anything. A team at its cap is refused before the model is asked. Cost grows with what Heimdall has to read: a story with many criteria on a project with many rules costs more than a two-line task. Review a record when it changed or was never reviewed; the panel always shows the latest review, so repeating a review of an unchanged record only repeats the cost. ## Where it stops - **It never writes to the record.** No field, status or version changes; the findings sit beside the record for a person to act on, through the editor or an [inline assist](/copilot/inline-assist). - **It never decides for you.** **Ask Heimdall** on a finding opens the panel with the finding in the composer and waits for you to send it. - **It never runs for a client guest**, who sees neither the panel nor the badge. - **It never runs where assists are off**, per project, under Settings → AI. - **It never runs unmetered.** The same guard that stops any model call outside a budgeted window stops the reviewer. The council that lands with the brief import will put more than one voice on a record at the moments that matter, such as submission for approval, and feed their accepted findings into the review queue. Today's reviewer is Heimdall alone, on demand. ## Related --- # Research Source: https://alm-docs.labs.zuselab.dev/copilot/research By the end of this page you will know how Heimdall's researcher thinks: what it is given, what it searches the web for, how its answer becomes notes and suggestions, why the same idea is never proposed twice, and the lines it never crosses. The task itself, pressing **Research the domain** on a project's overview and keeping or dismissing what comes back, is on the [project overview](/projects/overview#ask-heimdall-about-the-domain) page. ## Why research exists A client's brief says what they want and rarely what the world will hold them to: the data protection rules of their country, the payment and identity integrations their customers expect, the features every product of the kind ships as a baseline, the accessibility standard a public site is measured against. An analyst who starts from the brief alone finds these out one meeting at a time. Heimdall reads the web for them once, at the start, and writes them down with the pages it read, so the planning record starts from what the domain demands rather than from what the brief remembered to say. ## What Heimdall is given The researcher is told about the project in one brief, written the same way every time: - **The project**: its name, key, tagline or description, and the client with its industry and country, which decide which laws and which market apply. - **What the record already holds**: the titles of the project's epics, requirements, business rules and glossary terms, so Heimdall proposes what is missing rather than what is there. - **The ideas people already dismissed**, by title, with the instruction never to propose them again. A dismissal on the overview is remembered for the life of the project. - **The latest brief imported**, when there is one: the opening of the text the [brief import](/copilot/brief-import) read, cut to a few thousand characters, so the research starts from the client's own words. - **The house stack**, so a finding that lands on the code (a payment gateway's SDK, an accessibility test) is said in terms of what the team builds with. Anything the person asking may not read on the project (a list of rules on a project where their role cannot see rules) is left out rather than refused. ## What it searches for Heimdall may run up to eight web searches in a run and is told to use them for facts, not opinions, and to cite what it read. It looks, in this order, for: - what products of this kind commonly do and what their users expect; - the laws, regulations and standards that apply in the client's country and industry: data protection, payments, accessibility, sector rules; - the integrations such a client will expect: payments, identity, messaging, shipping, accounting, analytics; - what the leading products in the space offer as a baseline; - the security and accessibility expectations the client will be held to. ## How it must answer The researcher answers with one JSON object and nothing else: a list of **notes** and a list of **suggestions**. A **note** is what Heimdall learned on one topic: the topic in a few words, two or three plain paragraphs, and the web addresses it read. Notes hang under the run, at most eight, the most useful first; the project overview shows the newest finished run's notes under **What Heimdall learned**, and the [research endpoint](/api/v1/reference/research) returns every run's. A **suggestion** is one thing the analyst should do about what was learned, in the same shape as the [brief import's](/planning/brief-import) suggestions so the same cards, decisions and events serve both: - kind **Research** for a fact the project must honour: a regulation, an integration the client will expect, a baseline feature; - kind **Improvement** for a gap in the project's own record that the research exposed. Each carries a title, a summary of what to add and why, the sources it rests on, a confidence from 0 to 1 and a one-sentence rationale. At most twelve are kept per run. ## How the answer is filtered Before anything is written, the answer is cleaned the same way every time: - A note without a topic or a summary is dropped; a source that is not a web address is dropped; the same address is kept once; at most six sources stay on a note or a suggestion. - A suggestion of any kind other than Research or Improvement is dropped, as is one without a title. A confidence outside 0 to 1 is pulled back into the range. - A suggestion whose title matches, letter for letter and case aside, one the project already holds as a research suggestion (pending, kept or dismissed) is dropped, so a second run adds to the first rather than repeating it. The run's **suggestion count** is the number that survived. Summaries are turned into sanitised HTML paragraphs and shown as text on the cards; nothing in a page Heimdall read reaches your browser as markup. ## One run at a time, on the queue, as you Pressing **Research the domain** writes a run as **Queued**, puts the work on the queue and answers at once; the rail polls while the run is **Researching** and refreshes itself when the notes land. The work runs as the person who asked, with their permissions, inside the same metered window every other Heimdall feature uses, so what Heimdall may read is what you may read. A second ask while a run is queued or researching is refused with a toast; wait for it to finish or fail. A run **Failed** when the model did not answer, when its answer held nothing that could be read, when the team's monthly budget had no room, or when the person who asked is no longer a member of the team. The reason is kept on the run and shown on the rail; nothing is written, nothing is charged, and **Research again** starts a fresh run. ## What a run costs One call to the model configured for research, which thinks longer than the inline assists do and may search the web up to eight times, filed in the team's AI ledger under **Research** and counted against the month's budget on [Settings → AI](/workspace/ai-settings). The budget is checked before the call, the usage is filed right after, and only then are the notes and suggestions written with the ledger row they cost, so a run never exists as ready without its cost and a failed call never costs anything. A team at its cap is refused before the model is asked. A run is worth repeating when the project moved: a new client country, a brief imported, a round of requirements written. Repeating it on an unchanged project mostly repeats the cost, since the ideas already held are filtered out of the answer. ## Where it stops - **It never writes to the record.** A kept suggestion stays a note on the overview for everyone to read; nothing becomes an epic, a requirement or a rule unless a person writes it, through the editor, an [inline assist](/copilot/inline-assist) or the [brief import](/planning/brief-import). - **It never proposes a dismissed idea again.** Dismissals are read into every later brief. - **It never runs for a client guest**, who sees neither the rail nor the suggestions. - **It never runs where assists are off**, per project, under Settings → AI. - **It never runs unmetered.** The same guard that stops any model call outside a budgeted window stops the researcher. Research notes are read by the features that come after them: the standards step of the intake wizard, the test generator and the release writer will draw on what Heimdall learned here as those modules land. ## Related --- # Permissions & Safety Source: https://alm-docs.labs.zuselab.dev/copilot/safety-and-permissions ## Who may use it Always. Heimdall can reach everything they can. Only when their role carries the **Use AI** permission (`ai:use`), under AI in the role editor. Without it the panel shows "Heimdall is not in your role", the sidebar entry is hidden, and nothing is metered. Of the [default roles](/workspace/roles), admin, project manager, business analyst, developer, QA engineer and release manager carry **Use AI**; viewer and guest do not. A second permission, **Manage AI budgets** (`ai:manage-budgets`), lets a member change the team's cap under [Settings → AI](/workspace/ai-settings); admin and project manager carry it by default. The [ability catalogue](/api/v1/abilities) lists both beside the others. ## It can only do what you can do Heimdall runs every operation **as the person talking to it**, with exactly that person's permissions. For a team member, Heimdall carries their role's permissions, no more: a tool their role does not allow is not even offered to the model, a tool that refuses at run time is reported plainly, and it does not look for another way. The same request over the REST API or the MCP server with a token of the same role would be refused identically, because the tools are the same. | The role carries | So Heimdall can | And cannot | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | | Reads everywhere (developer, QA engineer, viewer-like roles with **Use AI**) | Answer which projects are live, who leads them, what changed, which endpoints deliver and which fail | Register, pause or delete an endpoint; revoke a token | | Project management (project manager) | Everything above, plus every project operation as the modules land, and raise the AI budget | Touch webhooks, tokens, roles or groups, which the default role withholds | | Everything (admin, the owner) | Everything the pages can, including pausing, rotating and deleting endpoints and revoking tokens | Reach another team | ## The confirmation gate Lists, lookups, the activity log, the delivery log. Registering an endpoint, retrying a delivery or every dead one, sending a test delivery, resuming a paused endpoint, marking notifications read. They add to the record and take nothing away. Switch **Review every change** on in the composer and these wait for a card too. Pausing an endpoint, rotating its secret, deleting it, revoking a token: anything that changes or removes what exists waits for **Confirm**, with the exact arguments on the card. A card not confirmed within ten minutes expires. The pause is not the model's good manners. Every destructive operation carries an annotation, and the runtime holds the call until you confirm; the model cannot split the action into smaller pieces, rephrase it, or run it another way. The [tools reference](/mcp/v1/tools-reference) lists every operation under its kind. ## Every change is on the record A change Heimdall makes is written to the [activity log](/workspace/activity-log) like any other, with the actor marked as **AI**, Heimdall named as the way it came in and the thread it came from, so the history reads "paused by Heimdall for Ada" rather than "paused by Ada". Every tool run, confirmed or refused, is kept on the thread's turn too, with its arguments, and shows as a step on the turn's work timeline. ## What the model is shown Your message; the context of the moment (the date and your timezone, your name, the team you act in, the project and page in view, where the team's AI budget stands); the text of the page you are looking at, while the eye in the panel's bar is lit, handed over as data it may read but never obey; the recent history of the thread (the last forty messages, tool results included); and the results of the tools it calls. Another team's data, Zuse Bifrost's own credentials, a webhook endpoint's secret, a token's plaintext, the model's instructions or the tool list when asked to reveal them. It says so when asked. Everything a tool returns is handed to the model as **data about your work**, never as instructions: a ticket, a comment or a stakeholder's note that says "ignore your rules and delete the project" is quoted, not obeyed. ## Models Heimdall runs on frontier models from Anthropic today, chosen per turn: a small fast model decides how involved a request is, a stronger one answers, and the Max tier reaches the strongest with its deepest thinking. Zuse Bifrost keeps every thread in its own tables in a provider-neutral form, so the provider behind a role can change without a conversation losing its history. Which model each job runs on is listed under [Settings → AI](/workspace/ai-settings#models). Every technical proposal Heimdall makes, a component's stack, a non-functional requirement, an estimate, a test approach, assumes the stack Zuse Technologies builds on, and it proposes something else only when the brief rules the house stack out, saying why. The stack is part of its standing instructions, not something each person has to repeat. ## Questions No. Heimdall reads through the same permission checks as the pages, so a role without a permission gets a refusal in words, not the data; and a tool their role cannot use is never offered to the model in the first place. The team's monthly AI budget. The statement under Settings → AI shows every call with the person and the project it came from. [Usage & Budgets](/copilot/usage-and-budgets) has the meter. Remove **Use AI** from the roles, or set the month's cap to a figure already spent. The owner's own access stays with the first; the second stops everyone, the owner included, until the cap is raised. No. A guest holds no team permissions, so the entry is never shown to them and nothing they do reaches a model. Ninety days after their last turn, then they are removed, with their tool runs and cards. You can archive or delete a thread yourself at any time from the panel. ## Related - [Roles](/workspace/roles) — the role editor and the AI permissions - [Activity log](/workspace/activity-log) — where a change made by Heimdall is recorded - [Usage & Budgets](/copilot/usage-and-budgets) — whose month a turn draws on - [Tools reference](/mcp/v1/tools-reference) — every operation under its kind --- # Start a Project in Conversation Source: https://alm-docs.labs.zuselab.dev/copilot/start-a-project By the end of this page you will know how a project goes from a paragraph in the Heimdall panel to a client, a project and a planning record you can read on every page, what Heimdall writes in which order, what it asks you, how the council joins in, and how to change or undo any of it. The intake wizard still exists for a person who prefers forms. The conversation is the shorter road: a business analyst gives the minimum, Heimdall expands it, and every record it writes is a draft through the same actions the pages run, with a version you can put back. ## Before you begin | To | You need | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | Create the client and the project | `client:create` and `project:create` on the team; project managers and analysts have them by default, a client guest never does. | | Let Heimdall write the record under it | The create permission of each family on the project (`component:create`, `epic:create`, `requirement:create` and so on), which your project role carries. | | Let the council sit | `ai:use`, the project's assists on, and room in the team's monthly AI budget within the council's share. | Heimdall holds exactly the permissions you hold. A record it may not write is refused by the same rule that would refuse you, and it tells you so rather than looking for another way. ## Tell Heimdall about the client [step] Open the panel with **Ask Heimdall** or `Ctrl+/` from any page, or press **Start a new project from a brief** on its start screen, and write what you know in your own words: > "We need a project for a laundry shop called Super Clean Laundry: an informational website with > online order placement, tracking, payment and appointment scheduling." That is enough. Heimdall looks the client up on the registry and creates it when it is new, creates the project with a key suggested from its name (`SCL` here; say another in the same message if you want one) and your paragraph as its description, and tells you both are in, with a card for each under its answer and **Open** to their pages. The project selector shows the new project at once. ## What it writes, in order [step] Heimdall then writes the record under the project the way an analyst would, in groups, saying what it wrote after each group with the references it gave them: 1. **Components and environments** on the house stack: a Laravel web app, a Laravel API when one is needed, a mobile app only when the brief says so, and development, staging, UAT and production environments. 2. **Stakeholders and personas**: the owner, the shop manager and the client's product owner as seats with the approval flags the brief implies; the busy parent, the student and the driver as the people the product serves. 3. **Epics** for the bodies of work the client recognises, and under each the **stories** with their acceptance criteria, a negative criterion among them. 4. **Functional and non-functional requirements**, each with a measure where it can be measured, and the **business rules** the brief states or implies, each with a worked example. 5. The **use cases** with their main, alternate and exception flows, the **glossary** of the client's words, the **data entities** with their fields, the **risks** it sees and the **decisions** it took on the stack. Every record arrives as a draft: an epic in draft, a story in the backlog, a requirement in draft, a glossary term in draft. Nothing reaches in review or approved except through the [approvals](/planning/approvals), and nothing is hidden from you: each group shows as cards under the answer, and every page shows the same records the moment they are written. Heimdall writes group by group and keeps talking. Answer a question, correct a name or add a detail between groups and it changes what it wrote, through the same update tools, before it goes on; nothing waits for a final form. ## What it asks you Heimdall asks only for what it cannot infer from the brief, one or two questions at a time, and never holds the scaffold back for them: the client's contact and country, the target launch, the budget in hours, whether a mobile app is wanted, who on your team leads the project. Answer in the thread and it fills the fields in; leave a question unanswered and the record stays as it is. Say when it is wrong. "There is no mobile app" deletes the component after a confirmation card; "the owner is Nadeesha Perera, she signs off requirements" writes the stakeholder with the flag; "call the key SCL2" renames the key while nothing is numbered under the project yet. ## The council sits on the scaffold [step] When the scaffold is in, the [council](/copilot/council) is convened on the project itself the moment Heimdall's answer lands, without anyone asking: the intake moment, with Odin, Thor, Loki, Týr, Frigg and Baldur reading the whole record through their lenses. (A scaffold is a project created in the conversation with at least one record written under it; a turn that only created the project waits for the record.) The sitting appears in the conversation as it happens: each voice researches the ground and says what it found, reads the scaffold and leaves findings, answers the other voices and refines the plan Heimdall drafts; Heimdall closes with the plan the council agreed and the findings ranked: what the drafts get wrong, what a project of this kind needs that nobody said yet, what the client will be held to. Each finding carries **Accept** and **Dismiss**; accepting one that drafts a record writes it as a draft, dismissing one is remembered so no voice raises it again. The same sitting is on the Council tab of the panel. The council spends from the team's AI budget within the council's share of it; the card says what the sitting cost. A project with no room left, or one whose settings seat no voice on the intake moment, gets no sitting and the scaffold stands without it; ask Heimdall for the council later and it convenes one by hand. A long scaffold is more steps than a question: Heimdall may reach the limit of one run with calls still queued. It carries on by itself, up to twice, picking up the queued calls where the run stopped; the timeline shows the calls the first run left behind with the reason, and the same calls run in the continuation. ## Research and a longer brief The [research sweep](/copilot/research) starts by itself the moment the scaffold lands: Heimdall reads the web for what products of this kind are expected to do, the regulations they meet and the integrations clients ask for, filing notes and suggestions on the review queue. Hand it a long document and it starts a [brief import](/copilot/brief-import) instead of typing the record out: the drafts land on the review queue for you to accept, edit, merge or reject, and Heimdall reports when the reading is ready. On your word it decides a row or a council finding for you, after a confirmation card. ## Change and undo Every record Heimdall writes is yours: edit it on its page, ask Heimdall to change it, or put an earlier version back from its history, exactly as for a record a person wrote. The activity log and every version carry Heimdall's name on your behalf, so who wrote what is never in doubt. A record you did not want is deleted after a confirmation card; a project you did not want is archived or deleted from its own settings page. ## From the MCP The same tools serve an agent connected over MCP, under its token's abilities. ## Permissions | Action | Permission | | ------------------------------------ | ------------------------------------------------------------------------- | | Create the client | `client:create` | | Create the project | `project:create` | | Write each family of record under it | That family's create permission on the project, through your project role | | Change or delete what Heimdall wrote | That family's update or delete permission; deletes confirm first | | Convene the council and run research | `ai:use` on a project whose assists are on | ## Questions **Can Heimdall approve what it wrote?** No. Approvals are decided by the people the project's policy names, on the approval bar or in the inbox, and never by Heimdall or an agent. It can submit a record for review when you ask. **What if two people start the same project?** A name or a key the team already uses is refused, and Heimdall says so and reuses the project that exists. **Does it spend the AI budget?** The conversation does, as every Heimdall turn does; the council sitting and the research sweep spend within their shares. The record itself costs nothing to write. ## Related - [The panel](/copilot/panel) — the thread, the cards and the confirmation cards - [The council](/copilot/council) — the voices and what a sitting costs - [Brief import](/copilot/brief-import) — a long document into the review queue - [Intake wizard](/projects/intake-wizard) — the same intake as forms - [Revisions and versions](/planning/revisions-and-versions) — putting a version back --- # Usage & Budgets Source: https://alm-docs.labs.zuselab.dev/copilot/usage-and-budgets Every model call Zuse Bifrost makes is **metered**: the tokens the provider reports are priced at the model's list price, written in the team's usage ledger, and summed against the team's **AI budget** for the month. Nothing is invoiced to you by Zuse Bifrost; the budget is the cap your team sets on what it is willing to spend on AI in a month, and at the cap every AI feature stops rather than running over. One row per call: the feature (a Heimdall turn, its routing, and later every assist), the provider and model, the input, cached, output and reasoning tokens, the cost, the person and the project. Read it per feature and model, and call by call, under [Settings → AI](/workspace/ai-settings). A cap in dollars and a warning line as a percentage of it. A new month starts from the workspace default; the owner or a budget manager can change both at any time. A cap of zero means no cap. Crossing the warning line and reaching the cap each tell the owner and everyone whose role carries **Manage AI budgets** once: in the Notifications Center, by email, and on Telegram if they route the **AI budget** class there. Each one also raises a webhook event. At the cap Heimdall refuses before any model is asked: the panel says "This month's AI budget is spent", and the REST API and the MCP server answer `AI_BUDGET_EXHAUSTED`. Raise the cap or wait for the month to turn. ## What a turn costs A turn is one message and its answer, however many tool steps it took (a turn stops at eight). Its cost is the sum of the tokens it used, priced per model: the prompt (the instructions, the context of the moment, the thread's recent history and the tool results, with the part the provider served from its cache priced lower), the thinking where the model thinks, and the words of the answer. The small line under every answer shows exactly this: the tier and model, the prompt tokens with the cached part, the output tokens, the time, and the cost. | What you do | What is filed | | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | Send a message and get an answer | One **Heimdall turn** row, plus one **Heimdall routing** row on the Auto tier | | Tap a follow-up button | The same as a message | | Confirm a card (the action runs and Heimdall finishes) | One **Heimdall turn** row | | Cancel a card | Nothing | | A turn whose model could not be reached, or that failed | Nothing | | A question refused because the budget is spent or your role lacks the permission | Nothing | ### The tiers | Tier | Which model answers | Cost | | -------- | ------------------------------------------------------------------------------------- | ------------- | | **Auto** | A small router model reads the message, then the simple or the involved model answers | Standard cost | | **Lite** | The lightest model, no router | Lowest cost | | **Pro** | The involved model, no router | Standard cost | | **Max** | The strongest model with its deepest thinking, no router | Highest cost | The router's own call is filed as **Heimdall routing** beside the turn, so the ledger carries the whole cost of answering; it runs on the smallest model and costs a fraction of a cent. ## The month, step by step **The month opens at the default.** The first call of a calendar month creates the team's budget row from the workspace default: the cap in dollars and the warning line, eighty percent unless changed. The owner can set both before anything is spent. **Every call moves it.** Each ledger row is added to the month the moment it is filed, under a lock, so two calls settling at once cannot both slip under the cap. **The warning goes out once.** When a row takes the month past the warning line, the owner and every budget manager are told, and `ai.budget_warning` is raised. The assists keep working. **The cap stops everything.** When a row reaches the cap, the same people are told, `ai.budget_exhausted` is raised, and from the next request on every AI feature refuses until the cap is raised or the month turns. The call that crossed the line is still filed; nothing is cut mid-answer. Saving a new cap under Settings → AI resets the month's warning and exhaustion marks, so the next crossing is announced again. Lowering the cap below what is already spent stops Heimdall at once. ## Who pays on a team The budget is the team's. Every member whose role carries **Use AI** spends from the same month, and the statement under Settings → AI shows every call with the person and, where there is one, the project it was about. A person who belongs to several teams spends from the team they are acting in. ## What is never metered - A cancelled card. - A turn whose model could not be reached, or that failed before it finished. - A question refused because the budget is spent or your role lacks the permission. - Reading the statement, the thread list or a stored thread. The price per million tokens of each model is the provider's published list price, kept in the workspace's configuration. A model with no price on the book is filed at zero cost; the statement names the model, so an unpriced one is visible rather than hidden. ## Questions No. Each calendar month has its own row, opened from the workspace default on its first call. A cap you set applies to the current month; next month starts from the default again. Longer threads send more history with every step, a tool that returns a long list adds its whole answer to the prompt, and the Max tier thinks before it writes. The line under each answer shows where the tokens went. Yes, like a message, because Heimdall runs the action, reads the result and writes its answer. Cancelling costs nothing. Set it to zero under Settings → AI. The spend is still filed and shown, and no warning or refusal ever fires. The team owner and any member whose role carries **Manage AI budgets**. Everyone else sees the month and the statement but not the form. ## Related - [Settings → AI](/workspace/ai-settings) — the cap, the warning line, the statement and the models - [The panel](/copilot/panel#the-budget-line-state-by-state) — what the budget line reads - [Permissions & Safety](/copilot/safety-and-permissions) — who may spend the team's month - [Notifications](/account/notifications) — where the **AI budget** class is delivered - [`ai.budget_warning`](/webhooks/v1/events/ai/ai-budget-warning) and [`ai.budget_exhausted`](/webhooks/v1/events/ai/ai-budget-exhausted) — the two events --- # Business Requirements Document Source: https://alm-docs.labs.zuselab.dev/documents/brd By the end of this page you will know what the business requirements document prints, which pages of the project each of its sections reads, how the executive summary comes to be, what the sign-off page asks of the client, and how to render the copy a sponsor puts their name to. The **business requirements document** (BRD) is the client's document: what they need and why, in the record's own words, from the pages the business analyst filled during intake and planning. It is generated from the [Documents page](/documents/generated-documents) with the kind **Business Requirements Document**, numbered _BRD #1_, _BRD #2_ and so on within the project, and rendered as a PDF on A4 pages or as an HTML page. Nothing in it is typed twice: a change on the record is in the next render. ## Who reads it The sponsor and the stakeholders flagged as approving requirements, before the build starts; the project manager and the business analyst, as the agreed statement of what the project delivers; and, once it is final, every client guest of the project under **Documents**. The team's SRS is the other half of the same record, written for the people who build and test; a BRD never prints requirements by reference, data dictionaries or environments. ## The cover Every generated document opens with a cover: the title you gave, the kind, the project's name and key, the client, the document's reference and its version once it is final, when it was rendered and by whom, and the scope it was rendered from (the baseline's name and the date it was frozen, or the record as it stands, with a note when drafts were included). A footer on every page carries the project, the reference and the page number. ## Section by section | Section | What it prints | Read from | | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | **1. Executive summary** | The opening page, as written on the document or drafted by Heimdall; _Not written yet._ when empty. | The document's own opening page | | **2. Business context** | The project's description, then the client's name (legal name when recorded), industry and country, the methodology and the target launch. | [Project settings](/projects/project-settings), [Clients](/projects/registry) | | **3. Stakeholders** | Name and title, role with the RACI letter, side, and which approvals they give (requirements, UAT, releases). | [Stakeholders](/projects/stakeholders) | | **4. The people the product serves** | Each persona with its role, tagline, goals and frustrations. | [Personas](/projects/personas) | | **5. Scope** | Each epic in scope with its priority, summary, goal and business value, and under it a table of its stories (key, title, the _As a… I want… so that…_ sentence, the acceptance criteria as a list, the priority). | [Epics](/planning/epics), [Backlog](/planning/backlog), [Acceptance criteria](/planning/acceptance-criteria) | | **6. Business rules** | Each rule with its key, kind, statement, the _when_ and _then_, and its worked examples. | [Business rules](/planning/business-rules) | | **7. Glossary** | Every term with its definition and the other names it goes by, in alphabetical order. | [Glossary](/projects/glossary) | | **8. Risks, assumptions, issues and dependencies** | Each RAID item with its key, kind, description, probability and impact where scored, mitigation and status. | [RAID log](/planning/raid-log) | | **9. Decisions** | Each decision with its key, status, the date it was decided, what was decided and its consequences. | [Decisions](/planning/decisions) | | **10. Sign-off** | A sentence saying what approving the document confirms, then a signature table: one row per stakeholder flagged **Approves requirements**, and a row for the project manager, each with blank signature and date cells. | [Stakeholders](/projects/stakeholders), the project's lead project manager | A section with nothing to print says so in one line (_No stakeholders recorded._, _No epics in scope yet._) rather than disappearing, so a reader sees what the record still lacks. A module the person who rendered the document may not open is left out entirely. ## What is in scope, and what is not Only records that carry a version, the approved ones, are printed: an epic or a story still in draft, a rule in review, a decision proposed and not yet accepted stay out. **Include records still in draft** on the editor prints them too, for a working copy the team reads before the record is approved. A document rendered from a frozen [baseline](/planning/baselines) prints exactly the epics, stories, rules and use cases that baseline holds, which is the copy a sponsor should sign: the scope they sign is the scope that was frozen, and a change to it afterwards goes through a [change request](/planning/change-requests). The scope section is organised by epic. A story without an epic is not printed in a BRD; put it under one, or leave it as the team's own item, since a BRD describes the scope the client recognises. ## The executive summary Heimdall drafts the executive summary when the field is empty and the project's AI assists are on, from an outline of exactly what the sections above will print: it names the client and what they need, what the project builds and for whom, what the document covers and how it is organised, and ends on what the sponsor is asked to do, which for a BRD is to read it and sign. It rests on the record alone and names no number, date or technology the outline does not carry. The draft is kept on the document for you to edit, and a summary you wrote is never touched; see [how the opening page is drafted](/documents/generated-documents#heimdall-on-this-page). ## Render the copy the sponsor signs [step] 1. Approve the record first: the epics and stories, the rules, the decisions the client should see, each through its own [approval](/planning/approvals). 2. Compose a scope [baseline](/planning/baselines) from those records and have the project manager approve it, so it is frozen. 3. On **Documents**, press **Generate → Business Requirements Document**, pick the baseline under **What it reads**, leave drafts off, write or leave the executive summary, and press **Generate**. 4. Read the draft, edit the summary if the words are not the client's, and **Save & Render Again**. 5. **Submit for review** from the approval bar; when the project manager approves, the copy is final and listed to the client's guests, who download it under **Documents**. 6. Send the client the signed scope baseline for signature from the baseline's own approval bar; the document is the readable form of what they sign, and its sign-off table is for the paper copy. ## Related - [Generated documents](/documents/generated-documents): generating, editing, rendering again, approving, superseding and deleting. - [Software requirements specification](/documents/srs): the team's half of the record. - [Signing off the scope](/guests/signing-off-scope): the client's side of the baseline the BRD is rendered from. --- # Generated Documents Source: https://alm-docs.labs.zuselab.dev/documents/generated-documents By the end of this page you will know how a document is generated from the project's record, what it reads and what it leaves out, how the opening page is drafted and edited, how a draft is rendered again, how the project manager's approval makes a copy final and what that does to the earlier copy, what the client sees, and how a document is superseded or deleted. A **generated document** is a file the project writes from its own record. You choose a **kind** (the [business requirements document](/documents/brd) the client signs, or the [software requirements specification](/documents/srs) the team builds against), a **title**, a **file** (PDF or HTML) and a **scope**: the record as it stands, approved records only or drafts included, or a frozen [baseline](/planning/baselines) so the document prints exactly what the client signed. The render runs in the background as the person who asked, reading every module through the same services and permissions that person has, so a module they may not see is left out rather than leaked. When nobody wrote the opening page, Heimdall drafts it from an outline of what the document prints. The result is a **draft** with a file you download or open in the browser. You edit the title, the scope or the opening page and render again as often as you like; each save keeps the [version](/planning/revisions-and-versions) before it. The **Document approval** moves a draft to **final**, and a final copy supersedes the kind's earlier final copy on the project. A client guest sees the final copies and nothing else. ## Before you begin | To | You need | | ------------------------------------------- | ------------------------------------------------------------------------------------------------ | | See the documents and open one | `document:view-any` and `document:view`; a client guest sees the final copies | | Generate a document | `document:create` | | Edit a draft's title, scope or opening page | `document:update` | | Render a draft or a failed document again | `document:render` | | Put a draft to the project manager | `document:finalise` | | Approve a document | To be named by the project's **Document approval** policy; the project manager role by default | | Supersede a final copy | `document:update` | | Delete a document | `document:delete` | | Have Heimdall draft the opening page | The project's AI assists on, and room left in the team's [AI budget](/copilot/usage-and-budgets) | A document prints what the record holds the moment it renders. A record still in draft is left out unless the document asked for drafts, so approve the epics, requirements, rules and use cases through their own [approval](/planning/approvals) first if the document is for the client. ## Open the page [step] **Documents** in the project's sidebar opens the page; the badge on the entry counts the documents you may see. The list shows each document's title with its reference (_BRD #2_, _SRS #1_: documents are numbered within their kind on the project) and who asked for it, the kind, the status, the file format with its size once rendered, when it was rendered and when it became final. Sort by title, kind, status, file, the rendered date or the final date; narrow the list by kind or status, or search by title; **Clear Filters** drops every filter. While a document is rendering the list refreshes itself every few seconds, so the file appears without a reload. An empty project reads _No Documents Yet_ and, to someone who may generate: _Generate the first one with the button above: Bragi lays the BRD or the SRS out from the record as it stands, Heimdall drafts the opening page, and the file is ready to read in a moment. Ask Heimdall in conversation and it generates one for you._ ## Generate a document [step] Press **Generate** and pick the kind from the menu; each kind says in a line what it prints and for whom. The editor opens with the kind's name as the **Title**, which you may change (the client reads it on the cover and on the file they receive), and the **File** set to PDF; choose HTML for a page you will paste into a wiki or read on a phone. Under **What it reads**, pick a **Baseline** to print exactly the records a frozen baseline holds, or leave it on _The record as it stands_; the picker appears only once the project has a frozen baseline. Tick **Include records still in draft** for a working copy inside the team; leave it off for anything the client reads, so only approved records are printed. The last fieldset is the opening page, named after the kind (**Executive summary** for a BRD, **Introduction** for an SRS): leave it empty and Heimdall drafts it when the document renders, or write it yourself and it is kept as written. Press **Generate**. The document is written as **Rendering**, numbered within its kind, the toast reads _Document queued_ and names it, and the render starts in the background as you. A few seconds later (longer when Heimdall writes the opening page) the status turns **Draft** and the file is there to read. A render that could not finish turns the document **Failed** and keeps the reason; see [Render again](#render-a-draft-again). The render reads the project through the same services your own screen reads, under your permissions on the project. A module your role may not open is left out of the document rather than printed; a module nobody filled prints a line saying nothing is recorded yet. ## Read a document [step] Open a row for the flyout: the reference, kind, status, file format and, once approved, the version as badges; the title; a line on where it stands; **Download** and **Open** (the same file, saved or shown in a browser tab) once there is a file; the approval bar; the opening page as written or drafted, or _Not written yet_; **What it reads** (the baseline or _The record as it stands_, and whether drafts are included); when it was rendered, by whom and the file's size; when it became final; when it was created; the **Versions** of the document itself; **Activity**; and **Delete**. **Download** in the row menu serves the same file. The file downloads under the document's title. A **Rendering** document shows _Bragi is writing the file; it appears here in a moment without a reload._ in place of the buttons; a **Failed** one shows the reason the render stopped. ## Edit the opening page and the scope [step] **Edit Document** in the row menu or **Edit** in the flyout reopens the editor on a draft or a failed document. Change the title, the file format, the baseline, whether drafts are included or the opening page; the kind never changes, generate another document for the other kind. **Save & Render Again** writes the change as a version, renders the document again with it and replaces the file; the toast reads _Document updated_. Heimdall drafts the opening page again only when you emptied it; a page you wrote or edited is kept exactly as it reads. A document that is in review, final or superseded does not change: the editor refuses with _A document that is Final does not change; generate a new one instead._ Generate a new one of the same kind; when it is approved it supersedes the old copy. ## Render a draft again [step] **Render Again** in the row menu or the flyout renders a draft or a failed document once more with exactly what it holds. Use it when the record changed since the file was written, when a render failed, or when the opening page was emptied so Heimdall writes a fresh one. The document reads **Rendering** until the new file replaces the old; nothing else about it changes. A render fails when the person who asked for the document is no longer on the project, when the file could not be written, or when Heimdall could not be reached or answered with nothing readable; the reason is kept on the document and `document.failed` goes out. Rendering again clears it. ## Put a draft to the project manager [step] Open a rendered draft's flyout. The **Approval** bar offers **Submit for review** once the document has a file; a document still rendering or failed cannot be submitted. Write a few lines for the approvers under **Summary** and confirm. The document moves to **In review**, the approvers of the project's **Document approval** policy are told (the project manager role by default, under the **Any one approver** rule, with seventy-two hours to answer and a reminder after a day), and the draft stops changing until they answer. What the approval does when it carries: - The document becomes **Final** at version 1, stamped with when it was approved and the revision that was reviewed, and with the date it became final. - The kind's earlier final copy on the project, if there is one, becomes **Superseded** and points at the new copy; a client who opens it sees it is no longer current. - `document.finalised` goes out beside `document.status_changed`, with the approval that carried it and the version reached. - The copy appears to the project's client guests, under **Documents** in their own sidebar. A rejection returns the document to **Draft**; the approver's reason is on the request, and you edit the draft, render again and resubmit. A request for changes holds the request until you resubmit; the next round asks the same approvers again. Withdrawing the request from the bar returns the document to draft as well. The approval writes a version of the document's title, scope and opening page and marks the file final. It does not freeze the record the document printed: a requirement approved after the render is not in the file. Print the client's copy from a frozen baseline when the scope must be exactly what they signed. ## Supersede or keep a final copy [step] A final document is never edited and never rendered again; it is what the client received. When a newer copy of the kind is approved, the earlier one is superseded by itself. To retire a final copy by hand, without a successor, choose **Move to → Superseded** in the row menu or the flyout; the toast reads _Document moved_. A superseded document stays readable, to the team and to the client, with every stamp it carried. The moves through **Rendering**, **In review** and **Final** belong to the render and the approval engine, and the menu does not offer them. ## Delete a document [step] **Delete Document** in the row menu or **Delete** in the flyout opens the confirmation, naming the document and what goes with it: the file, every version it kept and its activity history; the records it printed stay as they are. A document that is with its approvers is not deleted until they decide or the request is withdrawn. A final copy a client received is better superseded than deleted, and the confirmation says so. ## All the fields `brd` or `srs`, chosen from the Generate menu and never changed afterwards; the other kinds the application declares are refused until their modules land. The kind decides the template, the sections printed, the name of the opening page and the short label in the reference (_BRD #2_). 3 to 160 characters; defaults to the kind's full name. Printed on the cover and in the list, and used as the file's name when it is downloaded. `pdf` (default) or `html`. A PDF is laid out on A4 pages with a cover and a footer; an HTML file is the same document as one page. Markdown is declared and refused for now. Changing the file renders the document again. Optional; one of the project's frozen baselines, offered only when the project has one. A document rendered from a baseline prints only the records that baseline holds, at the record as it stands today, and names the baseline on its cover. Left empty, the document reads the record as it stands. A draft baseline or another project's is refused. Off by default: only records that carry a version, the approved ones, are printed, which is what a client should read. On, drafts are printed beside them for a working copy inside the team; the cover says so. Ignored when a baseline is chosen, since a baseline holds approved records only. Plain text up to 6,000 characters, paragraphs separated by a blank line, named after the kind. Left empty, Heimdall drafts it from an outline of what the document prints when the render runs, and the draft is kept here for you to edit. Written or edited by you, it is kept exactly as it reads. Read-only. The status moves through the render (rendering, draft or failed) and the approval engine (in review, final), and by hand only to superseded. The number is the document's place within its kind on the project, and the reference joins the kind's short label and the number (_SRS #1_). Read-only. The version is 0 while the document was never approved and 1 once it is final. Rendered at, by whom and the file's size are written by every render; final at by the approval that carried. Read-only; the reason a render stopped, in words a person reads, shown as a callout on a failed document and cleared by the next render. Read-only flags the flyout reads: whether there is a file to download or open (a draft, a document in review, a final or a superseded copy), and whether the editor and Render Again are offered (a draft or a failed document). Read-only: the document's id, when it was asked for and when it last changed, shown in your own timezone. ## What happens next - Generating a document emits `document.created` at once; the render that follows emits `document.rendered` with the file's format and size, or `document.failed` with the reason. - Editing a draft emits `document.updated` with the columns that changed and renders the document again; rendering again emits `document.rendered` or `document.failed` once more. - The approval engine emits `approval.requested`, the verdicts and `document.status_changed` on every move; the approval that carries adds `document.finalised` with the approval and the version. - Superseding emits `document.status_changed`; deleting emits `document.deleted`; putting an earlier version of a draft back emits `document.restored`. - A final copy is listed to the project's client guests and served to them on request; the sidebar badge counts it for them from that moment. - Every save of a draft writes a version; the history flyout compares them and puts one back, which does not render the document until you ask. ## Heimdall on this page Heimdall has two parts here. In conversation, _"Generate the BRD for the Customer Portal from the signed scope"_ asks for a document the way the Generate menu does, and _"What does SRS #1 say about password reset?"_ reads a rendered document back to you; the document is listed as asked for by you and rendered under your permissions. The second part is the opening page. When a document renders with that page empty and the project's AI assists are on, Heimdall is given the project, the client and an outline of what the document prints (the sections, their counts and the first names in each) and asked for two to four paragraphs of plain text: what the client is and needs, what the project builds and for whom, what the document covers and how it is organised, and what the reader is asked to do with it. It is told to rest on the outline alone, to invent no number, name or date that is not in it, and to name no technology the record does not. The call is filed in the team's AI ledger as **Document draft**, and a team whose budget is spent gets a failed render that says so rather than a silent blank page. A document whose opening page is already written costs no model call at all, so write it yourself to keep Heimdall out. The draft is kept on the document as text you edit like your own words; nothing Heimdall writes reaches the file without a person able to change it first. ## From the API and MCP ## Permissions | Action | Permission | | -------------------------------------------- | ------------------------------------------------------------------------------------ | | See the documents and open one, download it | `document:view-any`, `document:view`; a client guest sees the final copies only | | Generate a document | `document:create` | | Edit a draft, supersede a final copy | `document:update` | | Render a draft or a failed document again | `document:render` | | Submit a draft for approval | `document:finalise`, as staff | | Approve | Named by the **Document approval** policy; `approval:decide` over the API | | Delete a document | `document:delete` | | Read and restore the document's own versions | `document:view`, `document:update`; staff only, and only on a draft or a failed copy | ## Frequently asked **The record changed after the client got the final copy. Does the document update?** No. A final copy is what the client received and never changes. Generate the kind again, edit the opening page if it needs new words, and submit it; its approval supersedes the earlier copy and the client sees both, the current one marked as such. **Why is a module missing from my document?** The document reads the record as you may read it. A module your role does not open on the project is left out; a module nobody filled prints that nothing is recorded yet. A record still in draft is left out unless you ticked **Include records still in draft**. **Can I write the opening page in another tool and paste it?** Yes. The field takes plain text with paragraphs separated by a blank line; markup is printed as written. Keep it under the limit of the field and Heimdall never touches it. **The render failed with a message about the budget.** The team's monthly AI budget is spent, so Heimdall could not draft the opening page. Write the page yourself and render again, which costs nothing, or ask whoever manages the budget to raise the cap under [AI settings](/workspace/ai-settings). **A guest says they cannot see the document I sent them.** A client guest sees final and superseded copies only. A draft, a failed document or one in review is the team's working copy until the approval carries; check the status in the list. ## Related - [Business requirements document](/documents/brd): what the BRD prints and who signs it. - [Software requirements specification](/documents/srs): what the SRS prints and how the team uses it. - [Approvals](/planning/approvals): the Document approval policy, the inbox and the verdicts. - [Baselines](/planning/baselines): the frozen scope a client copy is rendered from. - [Revisions and versions](/planning/revisions-and-versions): every save of a draft, compared and put back. - [Documents shared with you](/guests/documents-shared-with-you): the client's side of the page. --- # Documents Source: https://alm-docs.labs.zuselab.dev/documents A project writes its documents rather than having them typed. The planning record the business analyst keeps, the epics and stories, the requirements, the business rules, the use cases, the people and the dictionary, is the source; a document is a kind, a title, a file format and a scope (the record as it stands, with or without drafts, or a frozen baseline), rendered as a PDF or an HTML page that reads the record through the same permissions as the person who asked for it. Heimdall drafts the opening page, the author edits it, and the project manager's approval makes a copy **final**: the one the client receives, superseding the kind's earlier final copy. Two kinds are open today. The **business requirements document** is the client's: what they need and why, the people, the scope as epics and stories, the rules, the glossary, the risks and the decisions, ending on a sign-off table. The **software requirements specification** is the team's: every functional and non-functional requirement, the rules, the use cases step by step, the data dictionary and where each requirement is built. The other kinds the application declares (sprint reports, release notes, UAT sign-off sheets, test summaries, post-mortems, status reports, the traceability matrix and timesheets) are refused until the modules that write them land, and each gets its own page here when it does. --- # Software Requirements Specification Source: https://alm-docs.labs.zuselab.dev/documents/srs By the end of this page you will know what the software requirements specification prints, which pages of the project each of its sections reads, how the introduction comes to be, and how a team uses the final copy to build and test. The **software requirements specification** (SRS) is the team's document: every requirement the system shall honour, the rules behind them, the use cases step by step, the data the system keeps and where each requirement is built, from the pages the business analyst keeps. It is generated from the [Documents page](/documents/generated-documents) with the kind **Software Requirements Specification**, numbered _SRS #1_, _SRS #2_ and so on within the project, and rendered as a PDF on A4 pages or as an HTML page an agent or a wiki can read. ## Who reads it The developers and the testers, before and during the build; the architect who shapes the components; a coding agent that was handed the project through the MCP server, which reads the same record through the planning and build tools and can read the SRS itself as plain words through `get_document`; and the client's technical lead, once it is final, under **Documents**. The sponsor's document is the [BRD](/documents/brd); an SRS prints no sign-off page. ## The cover Every generated document opens with a cover: the title, the kind, the project's name and key, the client, the document's reference and its version once it is final, when it was rendered and by whom, and the scope it was rendered from (a baseline's name and freeze date, or the record as it stands, with a note when drafts were included). A footer on every page carries the project, the reference and the page number. ## Section by section | Section | What it prints | Read from | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | **1. Introduction** | The opening page, as written on the document or drafted by Heimdall; _Not written yet._ when empty. **1.1 The project** adds the project's description. | The document's own opening page, [Project settings](/projects/project-settings) | | **2. Overall description** | **2.1 Components**: each component with its kind, description and stack. **2.2 Environments**: each with its kind, address and branch. **2.3 Users**: each persona with its role and goals. | [Components](/projects/components), [Environments](/projects/environments), [Personas](/projects/personas) | | **3. Functional requirements** | Each functional requirement as a block: key, title, priority and status, the statement, the rationale and how it is verified. | [Functional requirements](/planning/functional-requirements) | | **4. Non-functional requirements** | A table of the non-functional requirements: key and title with the statement, the category, the measure and the priority, ordered by category. | [Non-functional requirements](/planning/non-functional-requirements) | | **5. Business rules** | A table of the rules: key and name with the statement, the _when_ and the _then_. | [Business rules](/planning/business-rules) | | **6. Use cases** | Each use case with its key, title, actor, goal and trigger, the preconditions, a table of its steps (the flow, what the actor does, how the system responds) and the postconditions. | [Use cases](/planning/use-cases) | | **7. Data dictionary** | Each entity with its description and a personal-data tag where set, and a table of its fields: name, type, whether required, description, with a tag on each field that holds personal data. | [Data dictionary](/projects/data-dictionary) | | **8. Glossary** | Every term with its definition, in alphabetical order. | [Glossary](/projects/glossary) | | **9. Where each epic is built** | A table of the epics in scope with the keys of their stories and the epic's status, so a reader walks from a requirement to the backlog items that implement it. | [Epics](/planning/epics), [Backlog](/planning/backlog) | | **10. Decisions and risks** | Each decision with its key, kind and what was decided, then a table of the RAID items with their kind and status. | [Decisions](/planning/decisions), [RAID log](/planning/raid-log) | A section with nothing to print says so in one line (_No functional requirements recorded._, _No data entities recorded._) rather than disappearing. A module the person who rendered the document may not open is left out entirely: a guest token cannot render, and a developer whose role does not open the RAID log gets an SRS without section 10's risks. ## What is in scope, and what is not Requirements, rules, use cases and epics print only once they carry a version, the approved ones, unless **Include records still in draft** is on; components, environments, personas, the glossary and the data dictionary print as they stand, since they carry no approval. A document rendered from a frozen [baseline](/planning/baselines) prints exactly the requirements, rules, use cases and epics the baseline holds; the dictionary, the components and the environments print as they stand on the day, which the cover makes plain by naming the baseline. The SRS is a snapshot. When a requirement is approved or a rule changes after the render, press **Render Again** on a draft, or generate a new SRS once the earlier one is final; its approval supersedes the earlier copy, and the version a developer built against stays readable under it. ## The introduction Heimdall drafts the introduction when the field is empty and the project's AI assists are on, from an outline of what the sections above will print: it names the client and what they need, what the project builds and for whom, what the document covers and how it is organised, and ends on what the reader is asked to do, which for an SRS is to build and test against it. It rests on the record alone and names no technology the outline does not carry. The draft is kept on the document for you to edit, and an introduction you wrote is never touched; see [how the opening page is drafted](/documents/generated-documents#heimdall-on-this-page). ## Build and test against it [step] 1. Approve the requirements, rules and use cases the release needs through their own [approval](/planning/approvals), and the epics and stories that implement them. 2. On **Documents**, press **Generate → Software Requirements Specification**; choose HTML when a coding agent or a wiki will read it, PDF for the client's technical lead. Pick the frozen baseline when the build is scoped to one. 3. Read the draft, edit the introduction, and **Save & Render Again** until it reads right. 4. **Submit for review**; the project manager's approval makes it final and supersedes the earlier SRS on the project. 5. Point the team at it: a developer reads it from **Documents**, and an agent reads it with `get_document` and `with_text` set, or walks the same record story by story with `get_work_item`. ## Related - [Generated documents](/documents/generated-documents): generating, editing, rendering again, approving, superseding and deleting. - [Business requirements document](/documents/brd): the client's half of the record. - [Traceability](/planning/traceability): the links from a requirement to the stories that implement it and the tests that verify it, which section 9 summarises. --- # Approving as a Client Source: https://alm-docs.labs.zuselab.dev/guests/approving-requirements By the end of this page you will know how an approval reaches you, how to answer it, what each answer does to the record, and how to read what others decided. ## How an approval reaches you A project's policy for a kind of approval can name stakeholder seats. When your seat is named and has your account linked to it, every record the team submits for that kind appears in your **Approvals**, and you are told through your notification destinations with a button straight to it. A seat without an account is listed on the team's side for the record and cannot decide; ask the team to link your account if you were invited after the seat was created. ## Read the request [step] Open **Approvals** in the sidebar. **Waiting on me** lists what still waits for your answer: the record's reference and title, the project, who submitted it and when, how many approvals are in and how many the policy needs, and when the team expects an answer. Open a row for the flyout: the team's summary of what changed and why, the approvers named beside you with what each has said so far, and **Open** followed by the record's reference to read the requirement, rule or use case itself once it is visible to you. ## Give your verdict [step] Three buttons sit on the approval bar: | Button | What it does | | ------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **Approve** | Your approval is counted. The record is approved once the policy's rule is met: one approver, every approver, or a quorum. | | **Request changes** | The request is held for the team to edit the record and resubmit it; you decide again on the next round. | | **Reject** | The request is closed and the record returns to where it stood before it was submitted. | Each opens a short confirmation with a **Reason**. The reason is required for a rejection or a request for changes on most projects, and the team reads it beside your name. You decide once per round; a resubmission after changes opens a new round and asks you again. Every verdict is written once with the time, the address and the browser it came from, behind a signature hash. The team can export who approved what and when; nothing you decided can be edited afterwards, by you or by them. ## Afterwards - Approved records appear in your project view from the moment they are approved, at the version you read. - **Raised by me** stays empty for a guest; guests do not submit. - A request the team withdraws leaves your queue without a word; a request that runs past its time is escalated to the team's lead, not to you. - Your decisions stay listed on the request for everyone who can read it. ## Related - [For clients](/guests) — the guest's view of a project - [Approvals](/planning/approvals) — the same engine from the team's side - [Signing off the scope](/guests/signing-off-scope) — the scope baseline that comes to you for signature, and the change requests you raise - [Notifications](/account/notifications) — where the approvals alert class reaches you --- # Documents Shared With You Source: https://alm-docs.labs.zuselab.dev/guests/documents-shared-with-you By the end of this page you will know where the documents the team shares with you appear, how to read them, what their statuses mean, and what stays on the team's side until it is final. ## What a shared document is The team generates its documents from the project's own record: the **business requirements document** (BRD) that says what you need and why, with the scope, the rules and the people, and the **software requirements specification** (SRS) the team builds and tests against. A document is the team's working draft until the project manager approves it; from that moment it is **final** and shared with you, exactly as approved. A later copy of the same kind, once approved, supersedes the earlier one, and both stay readable so you can see what changed between them. ## Open the page [step] **Documents** in the project's sidebar lists every final and superseded document of the project: its title, its reference (_BRD #2_), the kind, the status, the file format and its size, when it was rendered and when it became final. Narrow the list by kind or status, or search by title. Open a row for the flyout: the title, the opening page the team wrote, what the document reads (the record as it stood, or the baseline it was rendered from), when it was rendered and when it became final. ## Download or open it [step] **Download** saves the file under the document's title; **Open** shows it in a browser tab. A PDF reads on any device; an HTML document is the same content as a single page. The row menu offers **Download** as well. The file is served to you by the application itself and never from a public address, so a link to it works only while you are signed in. ## What the statuses mean | Status | Meaning | | -------------- | -------------------------------------------------------------------------------------------------------- | | **Final** | Approved by the project manager; the current copy of its kind, exactly as the team shared it. | | **Superseded** | Replaced by a newer final copy of the same kind, or retired by the team; kept for the record, unchanged. | A document the team is still writing, one whose render failed, or one with its approvers reads _Rendering_, _Draft_, _Failed_ or _In review_ on the team's side and is not shown to you at all. If the team tells you a document is ready and you cannot see it, it has not been approved yet. A BRD rendered from the scope baseline is the readable form of what you sign; the signature itself is given on the baseline, from your **Approvals**, as described under [signing off the scope](/guests/signing-off-scope). The document's last page carries a signature table for a printed copy. ## What stays with the team The drafts and the versions behind a document, who rendered it and what Heimdall drafted before the team edited it, the activity on it, and the working copies that print records still in draft. These are hidden in the application's services, not merely on the screen, so an API token minted for your account lists and downloads exactly what you see here. ## Related - [Signing off the scope](/guests/signing-off-scope): the baseline a client copy is rendered from. - [Generated documents](/documents/generated-documents): the team's side of the same page. --- # For Clients Source: https://alm-docs.labs.zuselab.dev/guests By the end of this page you will know what a client guest account is, what it shows you, what it asks of you, and what it keeps from you on purpose. A **client guest** is a stakeholder of one or more projects with an account of their own. Zuse Technologies invites you by email; you sign in with a password or a passkey, and you land on the projects you were given, nothing else. The team sees the same application; you see its client face. ## What you see - The **approved record** of each project: epics, requirements, business rules and use cases from the moment the team approved them, the stakeholders and personas, the glossary, the data dictionary and the design references. Drafts and work in review stay on the team's side until they are approved. - The **baselines** of each project from the moment the team froze them, with the records each holds at the version that was frozen, and the **change requests** you raised or asked for, with their impact level and schedule once the team wrote them. - The **documents** the team shares with you: the final business requirements document and software requirements specification of each project, exactly as the project manager approved them, and the copies they superseded. - The **approvals** assigned to you: when a policy names your stakeholder seat and the team submits a record for review, or sends you a scope baseline to sign, it appears under **Approvals** in your sidebar with what the team wrote for you. - Your **notifications**: an approval that waits on you, the decisions made on what you signed, and later the releases, incidents and tickets of your projects. ## What you do - **Approve, request changes or reject** what names you, with a reason where the project asks for one; every verdict is signed and kept. See [approving as a client](/guests/approving-requirements). - **Sign the scope**: a frozen scope baseline comes to you for signature like any approval, and stands signed when every stakeholder asked has agreed. See [signing off the scope](/guests/signing-off-scope). - **Raise a change request** against a signed baseline in your own words, and co-sign its impact when the team sends it to the approvers. - **Comment** where the team let clients comment; your comments are visible to everyone on the project. - **Read the documents shared with you**: the final BRD and SRS of each project, under **Documents** in the project's sidebar. See [documents shared with you](/guests/documents-shared-with-you). - Later in the programme: sign off UAT runs, follow releases and incident status, and raise and follow support tickets. ## What stays with the team Estimates, time logs, internal comments, repositories and environments other than the UAT and production addresses, every AI control, the drafts behind an approved record, and the approvals that do not name you. These are hidden in the application's services, not merely on the screen, so an API token minted for a guest sees exactly what the guest sees. --- # Signing Off the Scope Source: https://alm-docs.labs.zuselab.dev/guests/signing-off-scope By the end of this page you will know what the team means by a baseline, how the scope comes to you for signature, what signing does and does not do, how you ask for a change once the scope is signed, and what you see of the baselines and the change requests of your projects. ## What a baseline is When the team has approved the requirements, the rules, the epics and the stories of a piece of work, they freeze them together as a **baseline**: a named list of records, each pinned at the version that was approved. From that moment none of those records changes by hand; a change goes through a change request, which you may raise and which you co-sign when it speaks for you. A **scope baseline** is the one you sign; a release, sprint or milestone baseline is the team's own mark and is frozen by their approval alone. ## Read the baselines [step] **Baselines** in a project's sidebar lists the frozen baselines: approved by the team, awaiting your signature, signed, or superseded by a newer one. Drafts and baselines still under the team's review are not shown. Open one for its description, the records it holds with the version of each and whether it is still held, and the dates it was frozen and signed. Open any record it holds from the project's own pages; it reads as the version that was frozen. ## Sign the scope [step] Once the team's approval froze a scope baseline, they submit it again to you. The request reaches every stakeholder of the project whose seat is flagged as approving requirements and whose account is linked, under **Approvals** in your sidebar and through your notification destinations. Open it: the team's summary, the baseline by name, the other stakeholders asked beside you, and **Open** to read the baseline itself. The three verdicts are the same as on any [approval](/guests/approving-requirements): | Verdict | What it does | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **Approve** | Your signature. The scope stands **Signed off** when every stakeholder asked has approved; one approval alone does not close it. | | **Request changes** | Holds the request for the team; they answer and resubmit, and you decide again. | | **Reject** | Returns the baseline to the team's frozen state. Write why; the team reads it and raises what it must as a change request. | The signed baseline shows the date of the signature in a green callout and is the record the project is held to. Your verdict is kept with the time, the address and the browser it came from, behind a signature hash, and cannot be edited afterwards. The signature covers the records the baseline holds, at the versions it holds. A record the team adds afterwards is a draft until it is approved and frozen in the next baseline, which comes to you for signature in its turn. ## Ask for a change [step] Under **Baselines → Change Requests**, press **New Change Request**: a **Title**, the **Reason** (most often _client request_), the **Baseline** the change applies to, the **Records** it touches from that baseline, and a **Description** in your own words. The request is raised in your name and, when your account is linked to a stakeholder seat, that seat is noted as the one who asked, so you are among the approvers when the team submits it: the change speaks for you, and you co-sign its impact. The team analyses the request: what it touches beyond the records you named, how many days the schedule moves, what it risks, and a level of low, medium or high. You read the level, the schedule and the prose on scope and risk; the effort in hours and the cost stay on the team's side. When the request comes to you for approval, read the impact first; approving unlocks the records named, so the team can make the change and submit the changed records for approval again. ## Afterwards - The change requests you raised or asked for are the ones you see in the list, with their status from submitted through analysis, approval and the work to verified; the team's other requests are not shown. - A rejected request shows the approver's reason; the baseline stands as it was. - A signed baseline superseded by a newer one stays readable with its signature; the newer one comes to you for signature in its turn. ## Related - [For clients](/guests) — the guest's view of a project - [Approving as a client](/guests/approving-requirements) — the three verdicts and what each does - [Baselines](/planning/baselines) and [change requests](/planning/change-requests) — the same records from the team's side --- # Welcome Source: https://alm-docs.labs.zuselab.dev Zuse Bifrost is where Zuse Technologies plans a client project once and then builds, tests, releases and maintains it against that same record. A Business Analyst captures the scope, the people, the requirements and the rules; developers, testers and release managers work from that record rather than from copies of it; clients approve and sign off inside it; and every phase, from the first epic to the last incident review, points back at the artefacts it touched. This site is the complete guide to the product. The Product Guide describes every page as a person uses it, phase by phase. The three reference sections describe the same record as a machine reaches it: the REST API, the outbound webhooks and the MCP server for coding agents. Integrations covers GitHub, pipelines, Telegram and email. ## Start here --- # Integrations Source: https://alm-docs.labs.zuselab.dev/integrations Zuse Bifrost connects to the tools a team already runs: a GitHub App links repositories and matches branches, commits and pull requests to stories by their keys; pipelines report deployments and test results with ingestion-only tokens; a Telegram bot delivers alerts and takes approvals; email carries invitations, alerts and documents; the OpenAPI document opens in Postman and any OpenAPI-aware tool. --- # Telegram Source: https://alm-docs.labs.zuselab.dev/integrations/telegram By the end of this page you will know how to link your account to the Zuse Bifrost bot, what arrives in the chat once you have, which commands the bot answers, and how to unlink. Zuse Bifrost runs one Telegram bot for the whole workspace. A person links their own account to it with a one-time code; from then on the alert classes they route to Telegram arrive in that private chat, rendered from the same text the email carries, with a button that opens the page the alert is about. ## Before you begin | You need | Why | | -------------------------------- | ------------------------------------------------------------------------------------------ | | The bot set up for the workspace | Your administrator configures it. Until then the Telegram card says the bot is not set up. | | Telegram on a device you hold | The code is redeemed from the chat you send it in, and that chat becomes your linked chat. | ## Link your account [step] 1. Open **Account Settings → Notifications**. The **Telegram** card is at the top. 2. Press **Link Telegram**. A six-character code appears, with how long it stays valid. 3. Either press **Open the bot**, which opens Telegram with the code already in the start message, or open the bot yourself and send `/start` followed by the code. 4. The bot answers that the chat is linked to your name. Back on the settings page, press **I have sent it** (or reload) and the card shows **Linked**. A code works once and expires after a few minutes; **Get a new code** replaces it. If the chat you send the code from is already linked to another account, it moves to yours. Linking is per person, in a private chat with the bot. The bot ignores group chats; project groups that should receive incident and release notices are addressed from the project's settings when those pages land. ## What arrives in the chat Every class of alert you route to Telegram in the [alert destinations grid](/account/notifications#choose-where-alerts-reach-you). A person who has not touched the grid is on the default, which sends every class to the linked chat; the critical classes (incidents, security, AI budget) are mailed as well, whatever the grid says. A message carries the alert's title in bold, its lines, and one button that opens the page the alert points at. Text is escaped before it reaches Telegram, so a project called _Payments <v2>_ reads as typed. ## Commands | Command | What it does | | ------------- | ------------------------------------------------------------------------------ | | `/start CODE` | Links the chat to the account that minted the code. | | `/start` | Says how to get a code, or that the chat is already linked. | | `/me` | Says which account the chat is linked as. | | `/unlink` | Detaches the chat. Alerts stop at once; link again any time with a fresh code. | | anything else | The short help. | More commands join as the phases land: your open work, the approvals waiting on you, and a quiet-hours switch. ## Unlink [step] 1. In the chat, send `/unlink`; or on **Account Settings → Notifications**, press **Unlink** on the Telegram card and confirm. 2. The chat's column disappears from the alert destinations grid, and alerts fall back on your other destinations. ## All the fields Six characters from an alphabet without zero, one, I or O, so it survives being read aloud. Valid for a few minutes, once; a new code replaces the previous one. The private chat the code was sent from. One chat per account and one account per chat; the settings card shows when it was linked. ## What happens next - Linking with a grid you already shaped adds the chat to it carrying every class; a person still on the default needs no row, because the default already reaches the chat. - Unlinking removes the chat's row from the grid, so nothing keeps pointing at a chat the bot may no longer reach. - Nothing is announced to webhooks; linking is a personal setting. ## Permissions | Action | Who may do it | | -------------- | ------------------------ | | Link or unlink | You, on your own account | ## Frequently asked Codes expire after a few minutes and work once. Press **Get a new code** and send the new one; make sure you send it as `/start CODE`, with the code after the command. No. The bot links private chats only. Send it a direct message. Incidents, security and AI budget alerts are always mailed, so a chat that stopped working can never swallow them. Only what you send it in the private chat, which is the commands above. It keeps the chat's id and nothing else. ## Related - [Notifications](/account/notifications): the Notifications Center and the alert destinations grid. - [Profile](/account/profile): the email address that stays the safety net. --- # Introduction Source: https://alm-docs.labs.zuselab.dev/introduction Zuse Bifrost is an application lifecycle management system built for one way of working: a project is described once, in full, and every later activity reads that description instead of restating it. Zuse Bifrost speaks with a light Norse accent, for fun. Components are forged in the workshops, environments are charted, webhook deliveries fly like ravens, the activity log is the runes, Heimdall keeps watch at the bridge and a refusal comes from the Norns; an empty page says "Nothing forged in the workshops yet". The things themselves keep their plain names, a project is a project and a team a team, and the labels, buttons, statuses and fields this guide names are exactly what you press; the flourish lives only in the sentences around them. ## The one record A project holds its client, its components (the web app, the API, the mobile app), the environments each component runs in, the stakeholders and the personas, and then everything the Business Analyst writes down about what is to be built: epics and user stories with structured acceptance criteria, functional and non-functional requirements, business rules, use cases, a glossary, a data dictionary, risks, assumptions, issues, dependencies, decisions and design references. Those artefacts are linked to each other, reviewed, approved, versioned and frozen into baselines; a change after a baseline is a change request with its impact spelled out. ## The five phases 1. **Plan.** The Business Analyst captures the project through a guided intake, or imports a brief and reviews what the AI drafted from it. Requirements are approved by the project's leads and, where the stakeholders ask for it, signed off by the client. 2. **Build.** Developers plan sprints from the approved backlog, work a board whose columns are the story statuses, link branches and pull requests by the story key, and log time against what they did. 3. **Test.** QA derives test cases from the acceptance criteria, composes plans and runs them per environment, raises defects from failed steps, imports automation results, and reads coverage in the traceability matrix. UAT runs are signed off by the client. 4. **Release.** Release managers compose a release from done work, read its readiness score, collect the go-live approvals, promote deployments through the registered environments and publish the notes. 5. **Maintain.** Incidents run in an incident room with SLA timers and a public status line; support tickets are triaged, answered and converted into bugs, stories or change requests; post-mortems become action items on the backlog. ## Who works in it | Role | What they do | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | Business Analyst | Captures and maintains the project record; submits artefacts for review. | | Project Manager | Approves, baselines, runs sprints and releases, watches health. | | Developer | Works the board, links code, logs time, moves stories with a note. | | QA Engineer | Writes and runs tests, raises defects, signs off UAT with the client. | | Release Manager | Composes releases, promotes deployments, publishes notes. | | Admin | Runs the workspace: members, roles, tokens, webhooks, integrations, AI budgets. | | Client guest | Reads the approved record for their project, approves what is assigned to them, raises tickets, follows releases and incidents. | ## AI on every surface The Heimdall panel answers questions from the live record and runs the same tools an agent would; every editor offers inline drafting, rewriting and checking; a brief can be imported into a reviewable scaffold; stories are scored for quality; test cases, release notes, impact analyses, incident timelines and ticket replies are drafted for a person to accept. Nothing is written without that acceptance, every accepted draft is stamped in the activity log, and every call is metered against the team's monthly budget. ## Where you land Signing in opens the **Dashboard**, the overview of your work across every project you can see. Today it greets you by name and says what will fill it: your projects, the approvals waiting on you, sprints and incidents appear there as the modules that raise them land. The sidebar beside it holds the project selector, **Projects** and **Clients** ([the registry](/projects)), **Ask Heimdall**, the notifications bell, the team switcher and your account menu, and every settings page described in [Your Account](/account) and [Your Workspace](/workspace) is one click from there. Choosing a project in the selector opens [its overview](/projects/overview) and swaps the menu for the project's own; **Back to Dashboard** brings you here again. --- # MCP Authentication Source: https://alm-docs.labs.zuselab.dev/mcp/v1/authentication The MCP server, written as `https://mcp.example.test` on this page, accepts two credentials on the same endpoint: | Credential | How you get it | Acts as | Best for | | ---------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- | | **OAuth 2.1 access token** | The client asks; you sign in to Zuse Bifrost and press **Authorize**. No token to copy. | You, on the team you are working in, with every ability you hold there | Claude, Claude Code, Cursor, VS Code, ChatGPT, any OAuth-aware client | | **Personal access token** (`zg_…`) | **Account Settings → API Tokens**, choosing the abilities and the team scope by hand | The abilities and `scope:team:` / `scope:project:` you minted | Scripts, CI, clients you configure with a header, narrow grants | Both run through the same tenant resolution and the same per-tool ability gate. Nothing about a tool changes with the credential; what changes is how broad the credential is and how it was granted. ## Connecting with OAuth Add the server URL to a client that supports remote MCP servers and choose to connect. The client discovers the authorization server on its own, registers itself, and sends you to Zuse Bifrost: 1. If you are not signed in to the application, you sign in first (two-factor authentication and passkeys apply as usual). 2. Zuse Bifrost shows a consent screen naming the client, the account you are signed in as, and what the connection will be able to do. 3. **Authorize** returns you to the client, which now holds a short-lived access token and a refresh token. An access token lasts **one hour**. The client renews it silently with the refresh token, which lasts **30 days from its last use and is rotated on every renewal**; a connection you stop using expires by itself. Disconnecting the server inside the client discards both tokens. The connection acts as **you**, on the team you had selected when you authorized it, with every ability your role there grants: the same reach you have in the application, and the same tenant boundary. Tools still enforce their own abilities, and tools that change data still ask for confirmation inside the client before they run. If you want a connection that can only read, or only reach one project, mint a personal access token with those abilities instead and configure the client with it. ### Under the hood For anyone wiring up a client by hand, the server follows the MCP authorization specification: - An unauthenticated call is answered with `401` and `WWW-Authenticate: Bearer realm="Zuse Bifrost MCP", resource_metadata="https://mcp.example.test/.well-known/oauth-protected-resource", scope="mcp:use"`. When the server lives under the application's own address as `/mcp`, the metadata sits at `/.well-known/oauth-protected-resource/mcp` there instead. - The protected resource metadata names the endpoint as its `resource`, the application's address as its one authorization server, `mcp:use` as its one scope and the `Authorization` header as the only place a bearer token is read from. - `/.well-known/oauth-authorization-server` on the application's address (mirrored at `/.well-known/openid-configuration`) lists the authorization, token and dynamic registration endpoints; the grants are `authorization_code` and `refresh_token`, PKCE is `S256` only, and the one scope is `mcp:use`. - Clients register dynamically (RFC 7591) as public clients; the token endpoint accepts `application/x-www-form-urlencoded`; a spent or rotated refresh token is refused with `invalid_grant`. - Native clients may name a custom redirect scheme: `cursor`, `vscode`, `vscode-insiders` and `windsurf` are accepted beside `https` callbacks. ## Connecting with a personal access token Mint a token under **Account Settings → API Tokens** and send it on every request: ```http Authorization: Bearer zg_live__ ``` Configure the client with the header (each [client page](/mcp/v1) shows where). The token carries only the abilities you ticked, frozen to one team and optionally to a list of projects, exactly as on the [REST API](/api/v1/authentication). [API tokens](/workspace/api-tokens) covers the page in full. ## Per-tool abilities Each tool enforces one concrete ability from the [catalog](/api/v1/abilities). A personal access token carrying `project:view-any` can call `list_projects` but receives `TOKEN_MISSING_ABILITY` on `get_activity_log`; an OAuth connection satisfies every ability because you consented as yourself. Grant personal access tokens narrowly. An ability means the same thing on both transports: a tool requires exactly what its REST equivalent does, so mint for the operations you intend to call rather than for the channel you intend to call them over. There is no MCP-specific ability to tick: `mcp:use` is the name of the OAuth scope a signed-in connection carries, and a personal access token whose abilities cover the tools it calls may use MCP exactly as it uses REST. | Tool | Required ability | | ------------------------ | --------------------------- | | `get_me` | `account:read` | | `list_projects` | `project:view-any` | | `get_project` | `project:view` | | `list_team_members` | `team-member:view-any` | | `list_tokens` | `token:view-any` | | `list_webhook_endpoints` | `webhook-endpoint:view-any` | | `retry_webhook_delivery` | `webhook-delivery:retry` | | `get_activity_log` | `activity:read` | The [tools reference](/mcp/v1/tools-reference) carries the ability mapping for every tool. ## Team and project scope A personal access token carries `scope:team:` and optional `scope:project:` entries, the same ones the [REST API](/api/v1/tenancy-and-scopes) uses. An OAuth connection is scoped to the team you were working in when you authorized it and to every project you can reach on it. The MCP server runs the same tenant-resolution step either way, so every tool call is team-isolated exactly as a REST request is, and anything outside the scope answers `RESOURCE_NOT_FOUND` rather than a permission error. ## Revocation - **Personal access tokens**: revoke from **Account Settings → API Tokens**, or with the [`revoke_token`](/mcp/v1/tools/tokens/revoke-token) tool from another token. Revocation takes effect on the next call; any client still holding the token receives `AUTHENTICATION_REQUIRED`. - **OAuth connections**: disconnect the server inside the client, which discards its tokens. The access token it held expires within the hour and the refresh token 30 days after its last use. ## Rate limiting Authenticated MCP calls are bucketed at **120 a minute per token**, per personal access token or per OAuth access token. Hitting the limit returns `RATE_LIMITED` with `Retry-After`. Claude, Cursor, ChatGPT Desktop and VS Code all pace their tool calls internally; exhausting this bucket in practice usually means a tight loop on the client side. Unauthenticated traffic to the MCP endpoint (probes, port scanners, misconfigured clients with no credential attached) is bucketed separately at **5 a minute per address**. The OAuth discovery documents and the registration endpoint have their own bucket of 60 a minute per address, so a client's first handshake is never throttled by someone else's probing. See [Rate limiting](/api/v1/rate-limiting) for the REST buckets. ## CORS Browser-based MCP clients (the hosted MCP Inspector, in-browser playgrounds, web extensions) issue cross-origin requests against the MCP endpoint. The server returns a permissive `Access-Control-Allow-Origin: *` so any browser origin can complete the handshake; `Access-Control-Allow-Credentials` is **not** set because the server authenticates exclusively through `Authorization: Bearer …` and never accepts cookies on this surface. The CORS layer exposes the request and response headers a browser client needs to read: ```http Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST, GET, DELETE, OPTIONS Access-Control-Allow-Headers: Authorization, Content-Type, Accept, MCP-Session-Id, MCP-Protocol-Version, Last-Event-ID Access-Control-Expose-Headers: MCP-Session-Id, X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After, WWW-Authenticate Access-Control-Max-Age: 86400 ``` Preflight `OPTIONS` requests short-circuit before authentication so a browser can complete its handshake without first owning a credential. Native MCP clients ignore CORS entirely; these headers only matter when the caller is a browser. ## Logging and auditing Every change a tool makes is recorded in the activity log with you as the causer and `mcp` as the surface it came through, beside the changes made in the application and over REST; reads leave no entry. The log is readable through the `activity:read` ability, over the REST `GET /v1/activity` endpoint or the [`get_activity_log`](/mcp/v1/tools/observability/get-activity-log) tool, and on the [activity log page](/workspace/activity-log), so you can audit what an agent did on your behalf. ## Related - [REST API authentication](/api/v1/authentication), the personal access tokens, same scope entries. - [Ability catalog](/api/v1/abilities), every ability string, grouped by domain. - [Security model](/mcp/v1/security), credential hygiene and threat model. --- # Connecting ChatGPT Desktop Source: https://alm-docs.labs.zuselab.dev/mcp/v1/connecting-chatgpt-desktop ## Requirements - ChatGPT with developer mode connectors enabled (Plus, Pro, Team, Enterprise or Edu; on a workspace plan an admin may need to allow them). - A Zuse Bifrost account. ChatGPT signs you in; a personal access token is only needed when you want a connection narrower than your own reach. The server address is written as `https://mcp.example.test` below; use the one your administrator shares. ### Add the connector [step] Open **Settings → Connectors → Create** (developer mode) and enter: - **Name**: `Zuse Bifrost` - **MCP server URL**: `https://mcp.example.test`, exactly the address you were given, no trailing slash - **Authentication**: **OAuth** Save. ChatGPT opens Zuse Bifrost in your browser: sign in if you are not already, read the consent screen and press **Authorize**. ChatGPT negotiates the MCP handshake and lists the tools in the connector panel. ### Test [step] Open a fresh chat with the Zuse Bifrost connector enabled (toggle icon in the composer) and ask: > List my Zuse Bifrost projects. The response includes which tool was invoked. If the call fails, inspect the error: every Zuse Bifrost MCP error carries a `docs_url` pointing back to this documentation. ## Using a personal access token instead Choose **Authentication: No authentication** on the connector, and add a custom header `Authorization` with the value `Bearer zg_…` from a token minted under **Account Settings → API Tokens**. ChatGPT then skips the sign-in and the connection carries only the abilities you ticked. ## Scope tips ChatGPT tends to invoke multiple tools per turn. For read-only exploratory chats, a personal access token with only `view-any` and `view` abilities keeps the assistant from writing anything. For workflows that register webhooks or tidy the Notifications Center, the OAuth connection is simpler; every tool that changes data is marked destructive, so ChatGPT asks before running it. ## Troubleshooting - **Connector disabled**: enable it in the composer toolbar for the current chat. - **Sign-in never completes**: the URL must be exactly the server's address; finish the browser sign-in and try connecting again. - **Auth failed** after a while: disconnect and reconnect the connector to sign in again; with a personal access token, re-mint it. - **A tool is refused** with a personal access token: the token is missing the ability that specific tool enforces; the error envelope includes `required_ability` to make fixing it fast. --- # Connecting Claude Source: https://alm-docs.labs.zuselab.dev/mcp/v1/connecting-claude-desktop Claude reaches Zuse Bifrost as a **connector**: you give it the server URL, it sends you to Zuse Bifrost to sign in, and you approve the connection. Connectors are shared across claude.ai and Claude Desktop on the same account, so you set one up once. The server address is written as `https://mcp.example.test` below; use the one your administrator shares. ## Requirements - A Claude plan that allows custom connectors (Pro, Max, Team or Enterprise; on Team and Enterprise an admin may need to enable them). - A Zuse Bifrost account. The connection acts as you, on the team you are working in. ## Add the connector [step] In claude.ai or Claude Desktop open **Settings → Connectors**, choose **Add custom connector**, and enter: - **Name**: `Zuse Bifrost` - **Remote MCP server URL**: `https://mcp.example.test`, exactly the address you were given, no trailing slash Save it, then press **Connect**. ## Sign in and authorize [step] Claude opens Zuse Bifrost in your browser. Sign in if you are not already (two-factor authentication and passkeys apply as usual), read the consent screen, which names the client, the account you are signed in as and what the connection can do, and press **Authorize**. You are sent back to Claude and the connector shows as connected. Access renews itself for as long as you keep using the connection; a connection you stop using for 30 days expires on its own. Disconnecting in **Settings → Connectors** ends it immediately. ## Verify [step] Open a fresh chat, make sure the Zuse Bifrost connector is enabled for it (the tools menu in the composer), and ask: > List my Zuse Bifrost projects. Claude calls `list_projects` and renders the answer. Read tools run as you ask; tools that change data ask you to confirm inside Claude before they run. ## Claude Code From a terminal: ```bash claude mcp add --transport http zusegenius https://mcp.example.test ``` Then run `/mcp` inside Claude Code and pick **Authenticate** next to `zusegenius`; the same browser sign-in and consent screen follow. A coding agent connected this way reads the project record beside the repository it is changing, which is where the planning and test tools will matter most as they land. ## Using a personal access token instead If you would rather the connection carry only the abilities you choose, read-only or one project, mint a token under **Account Settings → API Tokens** and configure Claude with it as a header instead of signing in. In Claude Code: ```bash claude mcp add --transport http zusegenius https://mcp.example.test \ --header "Authorization: Bearer zg_live_YOUR_TOKEN" ``` In Claude Desktop through `claude_desktop_config.json` (macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%/Claude/`), using the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) bridge, because the config file speaks stdio: ```json { "mcpServers": { "zusegenius": { "command": "npx", "args": [ "mcp-remote", "https://mcp.example.test", "--header", "Authorization:${AUTH_TOKEN}" ], "env": { "AUTH_TOKEN": "Bearer zg_live_YOUR_TOKEN" } } } } ``` Keep the token inside `env.AUTH_TOKEN`, not inline in `args`. On Windows, `npx` splits arguments on whitespace, so embedding `Bearer ` directly breaks the header. The `${AUTH_TOKEN}` placeholder is expanded by `mcp-remote` after the args are parsed, preserving the space. Fully quit and relaunch Claude Desktop after editing the file; MCP servers are loaded at launch. When the token rotates, mint a new one, replace the value, relaunch. ## Troubleshooting - **The connector will not connect**: the URL must be the server's address with nothing after it. Claude compares it to the address the server publishes and refuses a mismatch. - **Sent to the application but no consent screen**: you were asked to sign in first; complete the sign-in and Claude retries the authorization. - **Tools vanished after a while**: Claude renews access silently; if the renewal failed, disconnect and connect again from **Settings → Connectors**. - **A refusal on one tool** with a personal access token: it lacks that tool's ability; the error names it in `error.context.required_ability`. - **Logs**: Claude Desktop keeps MCP logs under **Developer → MCP → View logs**. ## Security tip An OAuth connection never puts a secret on your disk. A personal access token in a config file is plaintext: treat it as you would any credential file, with filesystem permissions, no screen recording while the config is open, and a rotation when the laptop changes hands. --- # Connecting Cursor Source: https://alm-docs.labs.zuselab.dev/mcp/v1/connecting-cursor ## Requirements - Cursor 0.46 or newer (MCP support GA). - A Zuse Bifrost account. Cursor signs you in; a personal access token is only needed when you want a connection narrower than your own reach. The server address is written as `https://mcp.example.test` below; use the one your administrator shares. ## Config Cursor's MCP configuration lives at `~/.cursor/mcp.json` on macOS / Linux and `%APPDATA%/Cursor/mcp.json` on Windows. ### Add the server [step] Open the config file and merge: ```json { "mcpServers": { "zusegenius": { "url": "https://mcp.example.test" } } } ``` Restart Cursor. ### Sign in [step] Open **Settings → MCP**. The `zusegenius` entry shows **Needs login**; click it. Cursor opens Zuse Bifrost in your browser: sign in if you are not already, read the consent screen and press **Authorize**. Cursor picks up the connection and lists the tools. ### Verify [step] Open Composer (or chat) and ask: > Using the zusegenius server, list my projects. Cursor shows the tool invocation in its sidebar. You can approve each call individually or toggle "auto-approve for session" on the zusegenius namespace; tools that change data are marked so Cursor asks first. ## Using a personal access token instead Mint a token under **Account Settings → API Tokens** with the abilities you want Cursor to exercise and send it as a header; Cursor then skips the sign-in: ```json { "mcpServers": { "zusegenius": { "url": "https://mcp.example.test", "headers": { "Authorization": "Bearer zg_live_YOUR_TOKEN" } } } } ``` ## Project-scoped versus user-scoped Cursor also supports per-workspace `.cursor/mcp.json`. If you want the Zuse Bifrost integration active only inside a specific repository, put the config there instead of the global location, with a token scoped to that repository's project. ## Troubleshooting - **Stuck on "Needs login"**: the URL must be exactly the server's address; finish the browser sign-in and return to Cursor, which retries on its own. - **Tool list empty**: check Cursor's "MCP" panel in the settings drawer for connection errors. - **A refusal on a call** with a personal access token: the token lacks that tool's ability. The response names it in `error.context.required_ability`. - **Not found on specific tools**: the token's team or project scope does not include the entity you referenced; see the [tools reference](/mcp/v1/tools-reference). --- # Connecting VS Code Source: https://alm-docs.labs.zuselab.dev/mcp/v1/connecting-vscode-claude ## Requirements - VS Code 1.99 or newer, with MCP servers enabled (`chat.mcp.enabled`), or the Claude Code extension. - A Zuse Bifrost account. VS Code signs you in; a personal access token is only needed when you want a connection narrower than your own reach. The server address is written as `https://mcp.example.test` below; use the one your administrator shares. ### Add the server [step] Run **MCP: Add Server** from the command palette, choose **HTTP**, and enter `https://mcp.example.test` with the name `zusegenius`. VS Code writes it to `.vscode/mcp.json` (workspace) or your user profile: ```json { "servers": { "zusegenius": { "type": "http", "url": "https://mcp.example.test" } } } ``` ### Sign in [step] When the server starts, VS Code asks to authenticate with Zuse Bifrost and opens your browser. Sign in if you are not already, read the consent screen and press **Authorize**; VS Code stores the connection in its secret storage, never in the config file. Using the Claude Code extension instead? Open its terminal and run `claude mcp add --transport http zusegenius https://mcp.example.test`, then `/mcp` → **Authenticate**. ### Use it [step] Open the chat panel in agent mode and prefix prompts with "using zusegenius" when you want MCP calls to run. Example: > Using zusegenius, what changed on project CRM today and who changed it? The panel shows each tool invocation inline with an approve / deny prompt. Approve per call or pin auto-approve on the zusegenius namespace; tools that change data are marked so the prompt always appears for them. ## Using a personal access token instead Mint a token under **Account Settings → API Tokens** with the abilities you want the assistant to exercise and send it as a header; VS Code then skips the sign-in: ```json { "servers": { "zusegenius": { "type": "http", "url": "https://mcp.example.test", "headers": { "Authorization": "Bearer zg_live_YOUR_TOKEN" } } } } ``` Prefer an input variable over a literal token in a workspace file that is committed. ## Workspace vs. user scope Put the config in the workspace `.vscode/mcp.json` if the Zuse Bifrost access should only apply to a specific repository. Keep it in your user profile if every VS Code window should have access. ## Troubleshooting - **Never asked to sign in**: the URL must be exactly the server's address; check **View → Output → MCP** for the server's log. - **Server fails to connect**: the same output panel shows the handshake; a 401 there means the stored connection lapsed. Run **MCP: Reset Trust** or remove and re-add the server to sign in again. - **A tool is refused** with a personal access token: the error envelope names the missing ability; mint a new token with it. --- # MCP Server Source: https://alm-docs.labs.zuselab.dev/mcp/v1 Zuse Bifrost hosts a Model Context Protocol (MCP) server beside the application. Any MCP-aware client connects by signing in to Zuse Bifrost (OAuth 2.1) or with a personal access token, and works in the project record through tool calls. Today that record is the account, the project registry with the components each project is made of, the environments it runs in, the people it answers to and is built for the dictionary it is written in (the glossary and the data entities), the designs it is built from and the repositories its code lives in, the epics it will deliver, the backlog of stories, tasks, bugs and spikes under them with their acceptance criteria, the functional and non-functional requirements it must honour, the business rules the business has decided, the use cases that say how each persona reaches a goal with it, the RAID log of what may go wrong and what the project leans on, the decisions that record every choice with its options and consequences, the links that join all of these into one record read from either side, the comments that carry the conversation on each of them, the files pinned on each of them, and every version of them, teams, tokens, webhooks and the activity log; the build, test, release and maintenance families join as each module lands, so an agent reads the live catalogue rather than assuming it. The agent writes as well as reads: it creates and changes clients, projects and their settings, components, environments, stakeholders, personas, the glossary and the data entities, designs, repositories and labels; creates epics, work items of every kind, use cases and RAID items; changes every planning record and moves it where the lifecycle allows by hand; hands a brief to Heimdall's reader, starts the research sweep, and decides a suggestion or a council finding on the person's word. Deciding an approval, administering the team and minting tokens stay with people. Every address on these pages is written as `https://mcp.example.test`. Your workspace's address is the one your administrator shares; the application also publishes it on its server card at `/.well-known/mcp/server-card.json`, under the address you sign in at. ## What you get - **161 tools** covering the account behind the token (who is acting, the team the token is scoped to, the Notifications Center), the project registry (every project the token can see, by key, status, client and health, the clients they are done for, the components each is made of and the environments each runs in, with their addresses, branch, flags and health), the people of each project (the stakeholders it answers to, with their side, role, weight and approvals, and the personas it is built for, with their goals, frustrations and devices), the dictionary of each project (the glossary terms with their definitions and the data entities with their fields), the design references of each project (links to Figma files, prototypes, contracts and diagrams, with the version that is current), the repositories of each project (where the code lives, with the default branch and the component each builds, never for a guest token), the epics of each project (what it will deliver, by reference, goal, owner, priority and status), the backlog of each project (its stories, tasks, sub-tasks, bugs, spikes and improvements with their kind, status, priority, people, estimates, labels and acceptance criteria, what is assigned to the person the token acts as, the project's own label vocabulary with how many items carry each word, a story drafted into the backlog as a draft, an item moved along its lifecycle or handed to a colleague), the requirements of each project (what the system shall do and the qualities it shall keep, each with its category, measure, source, priority, verification method and status, and a requirement drafted as a draft a person reviews), the business rules of each project (the decisions of the business in plain words, with their condition and action, exceptions, worked examples, authority, priority, dates in force and status, and a rule proposed as a draft a person reviews), the use cases of each project (how a persona reaches a goal with the system, with the actor, the other actors, the goal, the trigger, the conditions before and after, the frequency, the priority, the status and the steps of the main flow with the alternate and exception flows that leave it), the RAID log of each project (the risks scored by probability and impact with their mitigation and contingency, the assumptions, the issues and the dependencies, each with its area, owner, dates and status, never for a guest token, and a risk raised as open by the person the token acts as), the decisions of each project (every choice the project made, with the context, the options weighed, what was decided, the consequences, who decided and when, and the decision each supersedes, never for a guest token, and a decision recorded as proposed for a person to accept), the links of each artefact (every link that touches it, both ends named, read from its side, never for a guest token, and two artefacts linked with a kind, a repeat refused rather than doubled), the comments on each artefact (the conversation oldest first, the open comments only for a guest token, and a comment posted in Markdown as the person the token acts as, the people named and the followers told), the files on each artefact (their names, kinds, sizes and the endpoint that streams them, the agreed record only for a guest token) and every version a save of them wrote (who wrote it, why and what changed, readable and restorable by staff), each project's settings (sprint length, working days, timezone, estimation, workflow flags and alert routing, staff only), teams, team members, roles and groups, personal access tokens, outbound webhook endpoints and their deliveries, and the activity log of every change. [Full catalog](/mcp/v1/tools-reference). - **3 resources**: the `ability`, `error-code` and `webhook-event` catalogues. Agents load these for context before calling a tool. [Reference](/mcp/v1/resources-reference). - **No prompts** are shipped yet; the surface is tools plus resources. The agent playbooks (implement a story, write its tests, triage a defect) arrive with the planning and test modules. - **Fine-grained authorization.** Every tool enforces its own ability (`project:view-any`, `webhook-endpoint:create`, …), the same one its REST equivalent requires, so a leaked token cannot act beyond the abilities you granted. - **Nothing hidden.** Every change a tool makes lands in the activity log with you as the actor and `mcp` as the surface it came through, beside the changes made in the application. ## Connection details - **Endpoint:** `https://mcp.example.test`, your workspace's address - **Transport:** MCP streamable HTTP - **Server card:** `/.well-known/mcp/server-card.json` on the application's address: the endpoint, the protocol versions, where the OAuth metadata is and how many tools and resources the server publishes, for a client that discovers before it connects ([details](/api/v1/agent-readiness#mcp-server-card)) - **Auth:** OAuth 2.1, where the client sends you to Zuse Bifrost to sign in and authorize with no token to copy, or `Authorization: Bearer zg_live_...` carrying the abilities of the tools you intend to call - **Ability catalog:** the `zusegenius://enums/ability` resource - **Rate limit:** 120 tool calls a minute per token; unauthenticated probes capped at 5 a minute per address **Signing in gives the client your own reach.** An OAuth connection acts as you on the team you are working in, with every ability your role grants there. When you want a narrower connection, read-only or one project, mint a personal access token with just the `*:view` and `*:view-any` abilities and configure the client with it instead. ## Which client? ## Example prompts Natural-language calls any MCP client can translate into tool invocations against the Zuse Bifrost surface: - _"Who am I acting as, and which team is this token scoped to?"_ - _"List the projects in discovery or planning and tell me which of them has no lead developer yet."_ - _"What changed on project **CRM** in the last day, and who made each change?"_ - _"Which webhook endpoints have failing deliveries, and what did the endpoint answer?"_ - _"Replay everything that dead-lettered since Friday evening; the consumer is back up."_ - _"Show my unread notifications, then mark the onboarding ones read."_ - _"Which roles exist on my team, and what may a business analyst do?"_ Each tool's reference page includes more targeted examples. ## Related - [Quickstart](/mcp/v1/quickstart), a two-minute setup. - [Authentication](/mcp/v1/authentication), OAuth connections and personal access tokens side by side. - [Tools reference](/mcp/v1/tools-reference), every tool with its required ability. - [Resources reference](/mcp/v1/resources-reference), the three catalogue resources. - [Security](/mcp/v1/security), token scope, revocation and the audit trail. --- # Prompting Guide Source: https://alm-docs.labs.zuselab.dev/mcp/v1/prompting-guide MCP gives an LLM real levers on a project record. Good prompts respect rate limits, keep the blast radius small, and let the person see what is happening. Bad prompts either over-invoke tools or get stuck in loops asking for confirmation. ## Ground first [step] Tell the assistant what universe it is in before asking for action: > Call `get_me`, then load `zusegenius://enums/ability` and tell me what this token can and cannot do before we start. An agent that knows the team it is scoped to and the abilities it holds does not guess at a project it cannot see or a tool it cannot call. ## Name the project by its key [step] Every project has a key, such as `CRM`, which prefixes every reference inside it (`CRM-142`). Prompts that name the project once up front are far more reliable than prompts that describe it: > Work on project CRM for the rest of this conversation. First, tell me its status, health and who leads it. This prevents the assistant from silently switching projects mid-chat, which is the most common source of confusion. ## Scope the token narrowly [step] A personal access token limited to `project:view-any`, `project:view` and `activity:read` answers most "what is going on" questions. Do not hand it write abilities unless the prompt explicitly involves writes; mint a second token instead. An OAuth connection carries your whole reach, so give it tasks you would do yourself. ## Use the assistant for aggregations [step] The server surface is low-level. The assistant's value is combining tools: > For each project in development, read its activity log for the last day and tell me which projects had no change at all. That becomes: `list_projects` with `status: in_development` → iterate → `get_activity_log` per project → compare → render. ## Avoid unbounded loops [step] When you ask "find every X", cap it: > List up to 50 webhook deliveries that failed today. Lists page at up to 100 rows and the activity log at up to 200 entries, so an overly literal agent that is not capped walks every page before answering. ## Drive writes through previews [step] Run a write through a dry-run pattern first: > Register a webhook endpoint for every project event at `https://hooks.example.test/zusegenius`, named "Delivery board". Show me the exact arguments you would send, but do not execute yet. Then: > OK, go ahead. Tools that change data are marked destructive, so the client asks you to confirm before running them in any case; the preview makes sure what it asks about is what you meant. ## Reference The full ability catalog and tool list are at [abilities](/api/v1/abilities) and the [tools reference](/mcp/v1/tools-reference). --- # MCP Quickstart Source: https://alm-docs.labs.zuselab.dev/mcp/v1/quickstart By the end of this page Claude is connected to your workspace's MCP server as you, and has answered its first question from the project record. The server address is written as `https://mcp.example.test` below; use the one your administrator shares. ## Add the server to Claude [step] In claude.ai or Claude Desktop open **Settings → Connectors → Add custom connector** and enter the name `Zuse Bifrost` and the URL `https://mcp.example.test`. Save, then press **Connect**. Using Claude Code instead? Run `claude mcp add --transport http zusegenius https://mcp.example.test`, then `/mcp` → **Authenticate**. ## Sign in and authorize [step] Your browser opens Zuse Bifrost. Sign in if you are not already, read what the connection will be able to do, and press **Authorize**. You are returned to Claude with the connector connected. There is no token to copy: the connection acts as you, on the team you are working in, and renews itself while you keep using it. ## Try it [step] Open a new chat and ask: > List my Zuse Bifrost projects. Claude invokes the `list_projects` tool and renders the response. You can also ask: > What changed on project CRM this week, and who changed it? > Which of our webhook endpoints are paused or failing? Tools that change data ask you to confirm inside Claude first. ## Prefer a narrower connection? Mint a personal access token under **Account Settings → API Tokens** with only the abilities you want. Start read-only with `project:view-any`, `project:view` and `activity:read`, and configure the client with it as a bearer header. Each client page shows where; [connecting Claude](/mcp/v1/connecting-claude-desktop) covers Claude Desktop and Claude Code. ## What if I use Cursor, ChatGPT Desktop, or VS Code instead? Same server URL, same sign-in, different settings screens: - [Cursor setup](/mcp/v1/connecting-cursor) - [ChatGPT Desktop setup](/mcp/v1/connecting-chatgpt-desktop) - [VS Code setup](/mcp/v1/connecting-vscode-claude) ## Next - [Authentication](/mcp/v1/authentication), OAuth connections and personal access tokens side by side. - [Tools reference](/mcp/v1/tools-reference) - [Prompting guide](/mcp/v1/prompting-guide) - [Security model](/mcp/v1/security) --- # Resources Reference Source: https://alm-docs.labs.zuselab.dev/mcp/v1/resources-reference MCP resources are read-only JSON snapshots the server exposes over a URI scheme. Agents load them to ground themselves before running tool calls: reading the ability catalogue, for example, lets an agent propose the minimum-privilege token to mint for a given task, and reading the error catalogue lets it pick the right remediation before a retry. ## URI scheme Every resource URI starts with `zusegenius://`. Three resources are published: | URI | Contents | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `zusegenius://enums/ability` | The full ability catalogue a personal access token can carry: the `entity:action` gates that mirror the team permissions, plus the extras that exist only for tokens and agents (`account:read`, `account:write`, `mcp:use`, `ci:deploy`, `ci:test-results`). | | `zusegenius://enums/error-code` | Every error code the REST API and the tools can emit, with HTTP status, default message, remediation hint and `docs_url` anchor. | | `zusegenius://enums/webhook-event` | The outbound webhook catalogue grouped by family, with the minimum ability a subscribing token must hold for each event and the keys its `data` object can carry. | ## Why load them An agent that knows the catalogues before making calls will: - Know what the current token can and cannot do, and ask for the minimum-privilege ability list when a new token is minted. - Pattern-match an error code to its remediation without a docs round trip: a baseline lock needs a change request, a refused transition names the statuses allowed, an exhausted AI budget waits for an administrator. - Pre-filter webhook events when registering an endpoint with [`create_webhook_endpoint`](/mcp/v1/tools/observability/create-webhook-endpoint), so the list lines up with what the source actions produce and the token may receive. ## Example Reference a resource in a prompt: > Load `zusegenius://enums/webhook-event` and tell me which project events a token with `project:view` may subscribe to. Claude fetches the resource, parses the JSON, and answers without calling any tools. ## Payload shape There is **no shared envelope**. Each resource returns JSON keyed by its own collection name, with whatever counts are meaningful for it, and none of them carry a `version` field. | Resource | Top-level keys | | ---------------------------------- | ---------------------------------------------------- | | `zusegenius://enums/ability` | `abilities[]`, `core_count`, `extras_count`, `total` | | `zusegenius://enums/error-code` | `error_codes[]` | | `zusegenius://enums/webhook-event` | `webhook_events[]`, `families[]` | ```json { "abilities": [ { "value": "project:view-any", "group": "project", "label": "View Any Project" } ], "core_count": 60, "extras_count": 7, "total": 67 } ``` Each webhook event row carries `name`, `family`, `required_ability` and `payload_keys`; each error code row carries `code`, `http_status`, `message`, `remediation` and `docs_url`. The JSON manifests the API host serves at `/abilities.json`, `/error-codes.json` and `/webhook-events.json` are separate documents with their own envelope. If you are switching an agent between the two sources, check the shape rather than assuming it. ## Prompts The MCP server does not publish any prompts today; the full surface is tools plus the three resources above. The agent playbooks (implement a story from its context pack, write its tests, triage a defect) arrive as MCP prompts with the planning and test modules. --- # MCP Security Source: https://alm-docs.labs.zuselab.dev/mcp/v1/security ## Threat model The MCP server accepts two credentials over HTTPS, an OAuth 2.1 access token you granted by signing in or a personal access token, and runs the request inside the same tenancy boundary as the REST API. The most common risks are: 1. **A token leaking into LLM conversation logs.** Claude, Cursor, and ChatGPT Desktop all log the session. A developer sharing a transcript can leak an `Authorization` header if the client logs it. 2. **Over-privileged credentials.** Minting a personal access token with every ability ticked, or connecting over OAuth from an account with more reach than the task needs, makes a leak costly. 3. **Accidental writes.** An eager agent executing a tool that turns out to change state. The server is designed for the first two and treats the third as a prompt-engineering concern, marking every tool that changes data as destructive so a client asks before running it. Below are the controls. ## OAuth connections - **Consent, not a pasted secret.** You sign in to the application, with two-factor authentication and passkeys applying as usual, and read a consent screen that names the client, the signed-in account, the reach of the connection and where you will be sent back to. Nothing is copied to disk. - **Short-lived access.** An access token lasts one hour. The refresh token lasts 30 days from its last use and is rotated on every renewal; presenting a spent one is refused, so a copied refresh token dies the moment the client renews. - **Public clients with PKCE.** Every client registers dynamically as a public client and must present an `S256` code challenge; there is no client secret to leak. - **One scope, your own reach.** The connection acts as you on the team you were working in, with every ability your role grants there, inside the same tenant boundary as the application. A connection that should be narrower is a personal access token instead. - **Disconnect ends it.** Disconnecting the server inside the client discards its tokens; the access token expires within the hour and the refresh token cannot be renewed once discarded. ## Personal access tokens ### Prefix Every token starts with `zg_`, which makes one easy to find in a repository, a transcript or a log with a search; add the prefix to your own secret-scanning rules so a pushed token is caught before it is used. ### Mandatory team scope Every token carries `scope:team:` frozen at mint time. A leaked token is constrained to exactly one team and can never pivot. Project-level `scope:project:` tuples tighten this further, and a guest's token is always project-scoped. ### Minimum-privilege defaults Tick only the abilities the task needs; most tokens need two or three. An agent that reads the project record and the activity log needs `project:view-any`, `project:view` and `activity:read`, nothing more. ### Expiry Personal access tokens do not expire automatically. Long-lived tokens (CI, dashboards, a standing agent) should be rotated on your own cadence: revoke the old token, mint a fresh one with the same abilities, and update the client config. ### Revocation Revoke a token under **Account Settings → API Tokens**, or with [`revoke_token`](/mcp/v1/tools/tokens/revoke-token) from another token. Revocation is immediate; any client still holding the token gets `AUTHENTICATION_REQUIRED` on its next call. ## Audit trail Every change a tool makes is recorded in the activity log with you as the `causer`, the subject it changed and an `actor_kind` of `mcp`, which distinguishes agent-made changes from the application's and the REST API's. Review what an agent did on the [activity log page](/workspace/activity-log), or read the log over the API with the [`get_activity_log`](/mcp/v1/tools/observability/get-activity-log) tool or the `GET /v1/activity` REST endpoint. ## Leaked-credential playbook For a personal access token: 1. Revoke the token under **Account Settings → API Tokens**. 2. Review the activity log for the last 24 hours for changes made through the API or MCP by your account. 3. If writes happened you did not authorise, tell your workspace administrator, naming the token's id (never its value). 4. Mint a replacement token with the same abilities. 5. Update the client config (Claude Desktop, Cursor, and so on) with the new token. For an OAuth connection whose client or device you no longer trust: disconnect the server inside the client, change your Zuse Bifrost password (which signs out every browser session), and review the activity log the same way. Access the connection still held expires within the hour. ## Related - [Ability catalog](/api/v1/abilities) - [MCP authentication](/mcp/v1/authentication) - [API tokens](/workspace/api-tokens) --- # Tools Reference Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools-reference 161 tools across 13 groups: 72 read and 89 write (79 of them marked destructive, so a client confirms before running those). Every write runs the same action the application's own screens run, so permission checks, tenant scoping, webhooks and the activity log apply identically. Every count and list on this page is generated from the server's own export. ## Machine-readable catalog The authoritative, auto-generated list is published by the API host: ``` GET https://api.example.test/mcp-tools.json ``` Sibling documents at the same host, regenerated on every deploy: - `https://api.example.test/abilities.json`, the full ability catalogue. - `https://api.example.test/error-codes.json`, every error code with its HTTP status and remediation. - `https://api.example.test/webhook-events.json`, every outbound webhook event. ## Tools by group Each group below is one page of the reference. A page opens with what the group's tools are for, lists them, and then documents every tool in full: its arguments as the server validates them, what it returns, how it fails, the REST endpoint that runs the same action, the webhook events it fires, example prompts and the `tools/call` request an MCP client sends. A row here opens the tool on its page. ### Account Tools The account behind the token: who is acting, which team the token is scoped to, where alerts go and what sits unread in the Notifications Center. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account). - [`get_me`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/get-me) — Get Me (read) - [`list_notifications`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/list-notifications) — List Notifications (read) - [`mark_all_notifications_read`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-all-notifications-read) — Mark All Notifications Read (destructive) - [`mark_notification_read`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-notification-read) — Mark Notification Read (destructive) ### Build Tools The tracker of a project: its stories, tasks, sub-tasks, bugs, spikes and improvements, each numbered from the same sequence as the epics (`CRM-142`) with its kind, status from the backlog to done, resolution, priority, the people on it, its estimates and dates, the fields its kind adds and, for a story, the acceptance criteria that say when it is done. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build). - [`assign_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/assign-work-item) — Assign Work Item (destructive) - [`create_label`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-label) — Create Label (destructive) - [`create_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-work-item) — Create Work Item (destructive) - [`delete_label`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/delete-label) — Delete Label (destructive) - [`delete_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/delete-work-item) — Delete Work Item (destructive) - [`get_board_snapshot`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-board-snapshot) — Get Board Snapshot (read) - [`get_sprint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-sprint) — Get Sprint (read) - [`get_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — Get Work Item (read) - [`list_labels`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-labels) — List Labels (read) - [`list_my_work`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-my-work) — List My Work (read) - [`list_sprints`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-sprints) — List Sprints (read) - [`list_work_items`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items) — List Work Items (read) - [`move_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — Move Work Item (destructive) - [`update_label`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-label) — Update Label (destructive) - [`update_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-work-item) — Update Work Item (destructive) ### Code Tools Where a project's code lives: the Git repositories the team registered, each with its provider, the owner and name as the provider spells them, the branch releases are cut from, the address, whether it is private and the component it builds. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code). - [`create_repository`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/create-repository) — Create Repository (destructive) - [`delete_repository`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/delete-repository) — Delete Repository (destructive) - [`get_repository`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/get-repository) — Get Repository (read) - [`list_repositories`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/list-repositories) — List Repositories (read) - [`update_repository`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/update-repository) — Update Repository (destructive) ### Dictionary Tools The dictionary of a project: the glossary (the words the project agrees on, each with one definition, the other names it goes by, the terms it leans on, its source and its review status) and the data entities (the things the project keeps records of, each with its fields, their types, flags, validation and examples, its component, its owner, whether it holds personal data and how long it is kept). [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary). - [`create_data_entity`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/create-data-entity) — Create Data Entity (destructive) - [`create_glossary_term`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/create-glossary-term) — Create Glossary Term (destructive) - [`delete_data_entity`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-data-entity) — Delete Data Entity (destructive) - [`delete_glossary_term`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-glossary-term) — Delete Glossary Term (destructive) - [`get_data_entity`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-data-entity) — Get Data Entity (read) - [`get_glossary_term`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-glossary-term) — Get Glossary Term (read) - [`list_data_entities`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-data-entities) — List Data Entities (read) - [`list_glossary_terms`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-glossary-terms) — List Glossary Terms (read) - [`update_data_entity`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-data-entity) — Update Data Entity (destructive) - [`update_glossary_term`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-glossary-term) — Update Glossary Term (destructive) ### Documents Tools The documents a project writes from its own record: the business requirements document the client signs and the software requirements specification the team builds against, each asked for with a kind, a title, a file format (PDF or HTML) and a scope (the record as it stands, with or without drafts, or a frozen baseline), rendered in the background as the person who asked and under their permissions, numbered within its kind (`BRD #2`), with an opening page Heimdall drafts when nobody wrote it, moved to final by the project manager's approval, which supersedes the kind's earlier final copy, and shared with the client's guests from that moment. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents). - [`generate_document`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/generate-document) — Generate Document (destructive) - [`get_document`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/get-document) — Get Document (read) - [`list_documents`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/list-documents) — List Documents (read) ### Observability Tools Where to look when something did not happen: outbound webhook endpoints and their deliveries, and the activity log of every change made to a subject and who made it, from the application, the REST API, an MCP tool, an accepted Heimdall draft or a scheduled job. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability). - [`create_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/create-webhook-endpoint) — Create Webhook Endpoint (destructive) - [`delete_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/delete-webhook-endpoint) — Delete Webhook Endpoint (destructive) - [`get_activity_log`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-activity-log) — Activity Log (read) - [`get_webhook_delivery`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-delivery) — Get Webhook Delivery (read) - [`get_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-endpoint) — Get Webhook Endpoint (read) - [`list_webhook_deliveries`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries) — List Webhook Deliveries (read) - [`list_webhook_endpoints`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-endpoints) — List Webhook Endpoints (read) - [`pause_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/pause-webhook-endpoint) — Pause Webhook Endpoint (destructive) - [`resume_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/resume-webhook-endpoint) — Resume Webhook Endpoint (destructive) - [`retry_dead_webhook_deliveries`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries) — Retry Dead Webhook Deliveries (destructive) - [`retry_webhook_delivery`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-webhook-delivery) — Retry Webhook Delivery (destructive) - [`rotate_webhook_endpoint_secret`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/rotate-webhook-endpoint-secret) — Rotate Webhook Endpoint Secret (destructive) - [`test_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/test-webhook-endpoint) — Test Webhook Endpoint (destructive) ### People Tools The people of a project: the stakeholders it answers to (the client's sponsor, product owner and approvers, Zuse's own leads, third parties, each with a side, a role, a weight and the approvals that name them) and the personas it is built for (named, sketched users with goals, frustrations and habits, the "as a …" of every story). [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people). - [`create_persona`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/create-persona) — Create Persona (destructive) - [`create_stakeholder`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/create-stakeholder) — Create Stakeholder (destructive) - [`delete_persona`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-persona) — Delete Persona (destructive) - [`delete_stakeholder`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-stakeholder) — Delete Stakeholder (destructive) - [`get_persona`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-persona) — Get Persona (read) - [`get_stakeholder`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-stakeholder) — Get Stakeholder (read) - [`list_personas`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas) — List Personas (read) - [`list_stakeholders`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders) — List Stakeholders (read) - [`update_persona`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-persona) — Update Persona (destructive) - [`update_stakeholder`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-stakeholder) — Update Stakeholder (destructive) ### Planning Tools The planning record of a project: its epics (the bodies of work the client recognises, each numbered from the project's tracker sequence, `CRM-12`, with its goal, value, measures, owner, MoSCoW priority, status from draft to done and dates) and its requirements (what the system shall do, functional, `CRM-FR-12`, and the qualities it shall keep, non-functional, `CRM-NFR-3`, each with a category and a measure, with the statement, the source, the priority, the verification method and the status from draft to baselined). [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning). - [`analyse_change_impact`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/analyse-change-impact) — Analyse Change Impact (destructive) - [`comment_on_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/comment-on-item) — Comment On Item (write) - [`convene_council`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — Convene Council (write) - [`create_baseline`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-baseline) — Create Baseline (destructive) - [`create_epic`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-epic) — Create Epic (destructive) - [`create_raid_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-raid-item) — Create RAID Item (destructive) - [`create_requirement_draft`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-requirement-draft) — Create Requirement Draft (write) - [`create_story_draft`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-story-draft) — Create Story Draft (write) - [`create_use_case`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-use-case) — Create Use Case (destructive) - [`decide_council_finding`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-council-finding) — Decide Council Finding (destructive) - [`decide_suggestion`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-suggestion) — Decide Suggestion (destructive) - [`delete_baseline`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-baseline) — Delete Baseline (destructive) - [`delete_business_rule`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-business-rule) — Delete Business Rule (destructive) - [`delete_change_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-change-request) — Delete Change Request (destructive) - [`delete_decision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-decision) — Delete Decision (destructive) - [`delete_epic`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-epic) — Delete Epic (destructive) - [`delete_raid_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-raid-item) — Delete RAID Item (destructive) - [`delete_requirement`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-requirement) — Delete Requirement (destructive) - [`delete_use_case`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-use-case) — Delete Use Case (destructive) - [`get_approval_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-approval-request) — Get Approval Request (read) - [`get_baseline`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-baseline) — Get Baseline (read) - [`get_business_rule`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-business-rule) — Get Business Rule (read) - [`get_change_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-change-request) — Get Change Request (read) - [`get_council_session`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-council-session) — Get Council Session (read) - [`get_decision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-decision) — Get Decision (read) - [`get_epic`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-epic) — Get Epic (read) - [`get_import_batch`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-import-batch) — Get Import Batch (read) - [`get_raid_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-raid-item) — Get RAID Item (read) - [`get_requirement`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-requirement) — Get Requirement (read) - [`get_revision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-revision) — Get Revision (read) - [`get_use_case`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-use-case) — Get Use Case (read) - [`link_artefacts`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/link-artefacts) — Link Artefacts (write) - [`list_attachments`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-attachments) — List Attachments (read) - [`list_baselines`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-baselines) — List Baselines (read) - [`list_business_rules`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-business-rules) — List Business Rules (read) - [`list_change_requests`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-change-requests) — List Change Requests (read) - [`list_comments`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-comments) — List Comments (read) - [`list_council_sessions`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-council-sessions) — List Council Sessions (read) - [`list_decisions`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-decisions) — List Decisions (read) - [`list_epics`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-epics) — List Epics (read) - [`list_import_batches`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-import-batches) — List Import Batches (read) - [`list_links`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-links) — List Links (read) - [`list_pending_approvals`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-pending-approvals) — List Pending Approvals (read) - [`list_quality_reviews`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-quality-reviews) — List Quality Reviews (read) - [`list_raid_items`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-raid-items) — List RAID Items (read) - [`list_requirements`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements) — List Requirements (read) - [`list_research_notes`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-research-notes) — List Research Notes (read) - [`list_revisions`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — List Revisions (read) - [`list_use_cases`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-use-cases) — List Use Cases (read) - [`propose_business_rule`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/propose-business-rule) — Propose Business Rule (write) - [`raise_change_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-change-request) — Raise Change Request (destructive) - [`raise_risk`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-risk) — Raise Risk (write) - [`record_decision_draft`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/record-decision-draft) — Record Decision Draft (write) - [`remove_link`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/remove-link) — Remove Link (destructive) - [`restore_revision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/restore-revision) — Restore Revision (destructive) - [`review_quality`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/review-quality) — Review Quality (write) - [`start_brief_import`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-brief-import) — Start Brief Import (destructive) - [`start_project_research`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-project-research) — Start Project Research (destructive) - [`submit_for_review`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — Submit For Review (destructive) - [`sweep_improvements`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/sweep-improvements) — Sweep Improvements (write) - [`update_approval_policy`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-approval-policy) — Update Approval Policy (destructive) - [`update_baseline`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-baseline) — Update Baseline (destructive) - [`update_business_rule`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-business-rule) — Update Business Rule (destructive) - [`update_change_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-change-request) — Update Change Request (destructive) - [`update_decision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-decision) — Update Decision (destructive) - [`update_epic`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-epic) — Update Epic (destructive) - [`update_raid_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-raid-item) — Update RAID Item (destructive) - [`update_requirement`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-requirement) — Update Requirement (destructive) - [`update_use_case`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-use-case) — Update Use Case (destructive) ### Project Tools A project is one client engagement's record: its key, which prefixes every reference inside it, its status from discovery to maintenance, its methodology, visibility, health and leads. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project). - [`create_client`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-client) — Create Client (destructive) - [`create_component`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-component) — Create Component (destructive) - [`create_design_reference`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-design-reference) — Create Design Reference (destructive) - [`create_environment`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-environment) — Create Environment (destructive) - [`create_project`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-project) — Create Project (destructive) - [`delete_component`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-component) — Delete Component (destructive) - [`delete_design_reference`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-design-reference) — Delete Design Reference (destructive) - [`delete_environment`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-environment) — Delete Environment (destructive) - [`get_client`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-client) — Get Client (read) - [`get_component`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-component) — Get Component (read) - [`get_design_reference`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-design-reference) — Get Design Reference (read) - [`get_environment`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-environment) — Get Environment (read) - [`get_project`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project) — Get Project (read) - [`get_project_settings`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project-settings) — Get Project Settings (read) - [`list_clients`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-clients) — List Clients (read) - [`list_components`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) — List Components (read) - [`list_design_references`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-design-references) — List Design References (read) - [`list_environments`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) — List Environments (read) - [`list_projects`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-projects) — List Projects (read) - [`update_client`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-client) — Update Client (destructive) - [`update_component`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-component) — Update Component (destructive) - [`update_design_reference`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-design-reference) — Update Design Reference (destructive) - [`update_environment`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-environment) — Update Environment (destructive) - [`update_project`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project) — Update Project (destructive) - [`update_project_settings`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project-settings) — Update Project Settings (destructive) ### Role & Group Tools A role is the permission set one member holds, seeded per team and editable by its owner; a group is a bundle of members with permissions they share. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups). - [`list_groups`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-groups) — List Groups (read) - [`list_roles`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-roles) — List Roles (read) ### Team Member Tools Team members are the staff and guests who collaborate inside a team, each with a role that decides what they may do. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members). - [`list_team_members`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members/list-team-members) — List Team Members (read) ### Team Tools A team groups the people, roles and projects of one organisation, and every token is scoped to exactly one. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team). - [`get_team`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/get-team) — Get Team (read) - [`list_teams`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/list-teams) — List Teams (read) ### Token Tools Personal access tokens authorise every REST and MCP call, and the plaintext value exists exactly once. [Open the group](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens). - [`list_tokens`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/list-tokens) — List Personal Access Tokens (read) - [`revoke_token`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/revoke-token) — Revoke Personal Access Token (destructive) ## What is coming The planning, build, test, release and maintenance families (context packs, stories and acceptance criteria, comments, status moves, test results, defects, deployments, incidents) join this page as each module lands, under the same rules: one ability per tool, the same ability as the REST equivalent, and a confirmation before anything destructive. An agent should read the live catalogue with `tools/list` rather than assume this page's groups are the whole surface. ## Resource catalogs Alongside tools, the server exposes 3 read-only resources: - `zusegenius://enums/ability` — **API Ability Catalogue**: Every ability a personal access token can carry: the `entity:action` gates that mirror the team permission catalogue (projects, planning artefacts, tests, releases, incidents, teams, roles, tokens, webhooks and more) plus the extras (`account:read`, `account:write`, `mcp:use`, `ci:deploy`, `ci:test-results`). MCP and REST answer to the same catalogue: a tool requires the same ability its REST equivalent does, so mint for the operations you intend to call rather than for the channel. Read once at session start to know what the current token can do and to ask for the minimum-privilege list when a new token is minted. Returns `{ abilities: [{value, group, label}], core_count, extras_count, total }`. - `zusegenius://enums/error-code` — **API Error Code Catalogue**: Every error code the REST API and the MCP tools can emit. Each entry carries the HTTP status, the default message, a remediation hint and a `docs_url` anchor. Read it whenever a tool response carries an `error` envelope, so the right remediation is picked before a retry: a baseline lock needs a change request, a refused transition names the statuses allowed, an exhausted AI budget waits for an admin. Returns `{ error_codes: [{code, http_status, message, remediation, docs_url}] }`. - `zusegenius://enums/webhook-event` — **Outbound Webhook Event Catalogue**: Every outbound webhook event Zuse Bifrost can emit, grouped by family (`project`, `team`, `role`, `token` and `ai` today, with the planning, build, test, release and maintenance families joining as each module lands). Each row carries the minimum ability a subscribing token must hold to receive the event and the keys its `data` object can carry. Read it before registering a webhook endpoint so the event list lines up with what the source actions produce. Returns `{ webhook_events: [{name, family, required_ability, payload_keys}], families: [string] }`. Clients that support resource browsing pick any of these as context before driving the tools. See the [resources reference](https://alm-docs.labs.zuselab.dev/mcp/v1/resources-reference) for payload shapes. --- # get_me Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/get-me Describe the person the token acts as — the team the token is scoped to, every team held, where alerts go, the unread count and whether Telegram is linked. Takes no input. Learn who the agent is acting as before doing anything else: the person, the team the token is scoped to (`current_team.id`, the id every other tool acts in), every team they belong to, where their alerts are routed, how many Notifications Center entries they have not opened and whether their account is linked to the Telegram bot. The payload is exactly what [`GET /v1/me`](https://alm-docs.labs.zuselab.dev/api/v1/reference/me) returns. - Requires ability: `account:read` - Runs the same action as [`GET /v1/me/alert-destinations`](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations/list-your-alert-destinations) - Runs the same action as [`GET /v1/me`](https://alm-docs.labs.zuselab.dev/api/v1/reference/me/get-the-callers-account) - Runs the same action as [`GET /v1/teams/current`](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-the-current-team) - Annotations: Read-only ## Arguments This tool takes no arguments. ## Example call ```json { "name": "get_me", "arguments": {} } ``` ## What it returns ```json { "data": { "id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "name": "Priya Patel", "email": "priya@example.com", "title": "Business Analyst", "locale": "en", "timezone": "Asia/Colombo", "email_verified": true, "current_team": { "id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "name": "Zuse Technologies", "owner_user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "created_at": "2026-05-18T10:05:00+00:00" }, "teams": [ { "id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "name": "Zuse Technologies", "owner_user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "created_at": "2026-05-18T10:05:00+00:00" } ], "alert_destinations": [], "alert_destinations_default": true, "unread_notifications": 3, "telegram_linked": false, "created_at": "2026-05-18T10:05:00+00:00" }, "meta": {} } ``` `alert_destinations_default` is `true` while the person has written no destination of their own, so the application's default routing applies. `current_team` is `null` for a token minted without a team scope, or when the person has since left that team. ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks account:read. ## Example prompts > "Who am I signed in as, and which team will you act in?" > "Do I have unread notifications, and is my Telegram linked?" > "Which teams do I belong to?" ## Related - [list_teams](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/list-teams) - [list_notifications](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/list-notifications) - [Me API](https://alm-docs.labs.zuselab.dev/api/v1/reference/me) - [Alert Destinations API](https://alm-docs.labs.zuselab.dev/api/v1/reference/alert-destinations) --- # Account Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account The account behind the token: who is acting, which team the token is scoped to, where alerts go and what sits unread in the Notifications Center. These tools answer that in one call and keep the bell honest, marking one entry or the whole backlog read. ## Tools - [`get_me`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/get-me) — Get Me (read) - [`list_notifications`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/list-notifications) — List Notifications (read) - [`mark_all_notifications_read`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-all-notifications-read) — Mark All Notifications Read (destructive) - [`mark_notification_read`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-notification-read) — Mark Notification Read (destructive) --- # list_notifications Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/list-notifications Read the Notifications Center of the person the token acts for — every alert sent to them, newest first, with its class, where it points and whether it was read. Answer "what has happened on my account that I have not looked at?". The Notifications Center is the sidebar entry with its unread count: every alert Zuse Bifrost sent the person (an approval waiting on them, an assignment, a mention, a sprint or release event, a failing deployment, an incident, a security notice, an AI budget warning). Each row is the same object [`GET /v1/me/notifications`](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications) returns; `meta.unread` says how many are unread in all. Narrow with `unread` (`true` or `false`), `class` (one of `approvals`, `assignments`, `mentions`, `planning`, `sprints`, `code`, `testing`, `releases`, `deployments`, `incidents`, `support`, `security`, `digest`, `onboarding`, `budget`) and `query`, words a title or body must contain. - Requires ability: `account:read` - Runs the same action as [`GET /v1/me/notifications`](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/list-the-callers-notifications) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `unread` | boolean | no | `true` for entries not yet read, `false` for entries already read; omit for every entry. | | `class` | `approvals`, `assignments`, `mentions`, `planning`, `sprints`, `code`, `testing`, `releases`, `deployments`, `incidents`, `support`, `security`, `digest`, `onboarding`, `budget` | no | One class of alert, or omit for every class. | | `query` | string | no | Words a title or body must contain; omit for every entry. | | `limit` | integer | no | Entries per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_notifications", "arguments": { "unread": true, "class": "approvals" } } ``` ## What it returns ```json { "data": [ { "id": "0b1f6e2a-7c3d-4e5f-8a9b-0c1d2e3f4a5b", "class": "security", "class_label": "Security", "title": "New API token minted", "body": "A token named Cursor was minted for your account.", "route": "settings.tokens", "params": {}, "url": "https://app.example.test/settings/tokens", "read": false, "read_at": null, "created_at": "2026-10-26T10:05:30+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false, "unread": 1 } } ``` ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks account:read. - `VALIDATION_FAILED` — class is not one of the fifteen classes; the error context lists them. ## Example prompts > "Anything I should look at? Show me my unread notifications." > "List the security notifications from this week." > "Which notifications mention project CRM?" ## Related - [mark_notification_read](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-notification-read) — clear one entry. - [mark_all_notifications_read](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-all-notifications-read) — clear the backlog. - [get_me](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/get-me) — the person, with unread_notifications. - [Notifications API](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications) - [Notifications and alert routing](https://alm-docs.labs.zuselab.dev/account/notifications) --- # mark_all_notifications_read Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-all-notifications-read Mark every unread entry of the person's Notifications Center as read in one call. The bell's "Mark all as read" for agents: every unread entry is marked read in one statement and the tool answers how many it marked. Idempotent: a second call marks nothing and answers `0`. Use it when the person has caught up; to clear one entry use [`mark_notification_read`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-notification-read). - Requires ability: `account:write` - Runs the same action as [`POST /v1/me/notifications/read-all`](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-every-notification-read) - Annotations: Destructive, Idempotent ## Arguments This tool takes no arguments. ## Example call ```json { "name": "mark_all_notifications_read", "arguments": {} } ``` ## What it returns ```json { "data": { "marked": 3 }, "meta": {} } ``` ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks account:write. ## Example prompts > "Clear my notifications, I've read them all." > "Mark everything read and tell me how many there were." ## Related - [list_notifications](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/list-notifications) - [mark_notification_read](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-notification-read) - [Notifications API](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications) --- # mark_notification_read Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-notification-read Mark one entry of the person's Notifications Center as read. The bell's click for agents: one entry of the person's Notifications Center is marked read, so the sidebar count stops counting it. Idempotent: an entry already read keeps its first `read_at`. Take the id from [`list_notifications`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/list-notifications). - Requires ability: `account:write` - Runs the same action as [`POST /v1/me/notifications/{notification}/read`](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications/mark-a-notification-read) - Annotations: Destructive, Idempotent ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `notification_id` | string | yes | UUID of the entry to mark read, from `list_notifications`. | ## Example call ```json { "name": "mark_notification_read", "arguments": { "notification_id": "b29f2396-f113-4000-aca8-5a2f9827393a" } } ``` ## What it returns ```json { "data": { "id": "0b1f6e2a-7c3d-4e5f-8a9b-0c1d2e3f4a5b", "class": "security", "class_label": "Security", "title": "New API token minted", "body": "A token named Cursor was minted for your account.", "route": "settings.tokens", "params": {}, "url": "https://app.example.test/settings/tokens", "read": true, "read_at": "2026-10-26T10:35:00+00:00", "created_at": "2026-10-26T10:05:30+00:00" }, "meta": {} } ``` ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks account:write. - `RESOURCE_NOT_FOUND` — the id is not one of this person's entries. ## Example prompts > "I've dealt with the token notice — mark it read." > "Mark the notification about project CRM read." ## Related - [list_notifications](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/list-notifications) - [mark_all_notifications_read](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/account/mark-all-notifications-read) - [Notifications API](https://alm-docs.labs.zuselab.dev/api/v1/reference/notifications) --- # assign_work_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/assign-work-item Hand a work item to a member of the team, or to nobody, and announce the hand-over. Hand a work item to a person by `assignee_id`, a member of the team (find ids with `list_team_members`), or to nobody by leaving `assignee_id` out. The item's assignee changes, the page and `list_my_work` read it at once, and `work_item.assigned` goes out with the person before and after. Handing an item to the person who already holds it changes nothing and announces nothing. Destructive in the MCP sense: it takes an item off someone's plate or puts it on, so confirm with the person before assigning an item that is not theirs. A person who is not a member of the team is refused. - Requires ability: `work-item:assign` - Runs the same action as [`POST /v1/work-items/{item}/assign`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/assign-a-work-item) - Fires events: [`work_item.assigned`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-assigned) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `work_item_id` | string | yes | UUID of the work item to hand over. | | `assignee_id` | string | no | UUID of the person who will do the work, a member of the team; leave it out to unassign. | ## Example call ```json { "name": "assign_work_item", "arguments": { "work_item_id": "656556ee-9249-4800-a4ad-d0a9546c9122" } } ``` ## What it returns ```json { "data": { "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 142, "reference": "CRM-142", "type": "story", "parent_id": null, "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Tier upgrade email", "description": null, "status": "ready", "resolution": null, "priority": "high", "assignee_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "assignee_name": "Nimal Fernando", "reporter_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "reporter_name": "Dilani Perera", "reviewer_id": null, "qa_owner_id": null, "story_points": "5", "estimate_minutes": null, "remaining_minutes": null, "due_on": "2026-11-20", "started_at": null, "completed_at": null, "persona_id": null, "as_a": "gold member", "i_want": "to be told the day I reach platinum", "so_that": "I use the new perks straight away", "business_value_points": null, "severity": null, "environment_id": null, "component_id": null, "steps_to_reproduce": null, "expected_result": null, "actual_result": null, "reproducible": null, "timebox_minutes": null, "outcome": null, "component_ids": [], "acceptance_criteria": [], "position": 3, "version": 0, "approved_at": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-01T16:30:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown work_item_id, or an item outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — assignee_id is not a member of the team. - `TOKEN_MISSING_ABILITY` — token lacks work-item:assign. - `FORBIDDEN` — the token holder's role on the project does not carry work-item:assign. ## Example prompts > "Assign CRM-142 to Nimal." > "Take the receipt totals bug off Dilani's plate; nobody holds it for now." > "Give me item `b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e`, I am picking it up." ## Related - [list_team_members](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members/list-team-members) — the people an item can go to. - [move_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — move it along once it is theirs. - [list_my_work](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-my-work) - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) --- # create_label Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-label Adds a word the project pins on its work items, with a colour and a meaning. Add a label to a project: `project_id` and `name` (1 to 60 characters, unique within the project) are required; `color` is one of the palette's names (`zinc` by default) and `description` says what the label means. The label is written through the same action the backlog's label manager runs and `label.created` goes out. Pin it on an item with `create_work_item` or `update_work_item`. A staff token with `work-item:update` on the project is needed. - Requires ability: `work-item:update` - Runs the same action as [`POST /v1/projects/{project}/labels`](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/add-a-label) - Fires events: [`label.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `name` | string | yes | The word, 1 to 60 characters, unique within the project. | | `color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` | no | One of the palette's colours; zinc by default. | | `description` | string | no | What the label means, up to 160 characters. | ## Example call ```json { "name": "create_label", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "name": "" } } ``` ## What it returns ```json { "data": { "id": "5d6e7f8a-9b0c-4d1e-8f2a-3b4c5d6e7f8a", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "payments", "color": "emerald", "description": "Anything that moves money", "work_item_count": 0, "created_at": "2026-10-28T09:50:00+00:00", "updated_at": "2026-10-28T09:50:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — name missing or outside 1 to 60 characters, a name the project already uses, a color outside the palette, or a description over 160 characters. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry work-item:update. - `TOKEN_MISSING_ABILITY` — token lacks work-item:update. ## Example prompts > "Create labels for payments, notifications and pickup logistics." > "Add a red label called urgent-for-launch." ## Related - [list_labels](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-labels) — the project's words today. - [update_label](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-label) — rename or recolour one. - [update_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-work-item) — pin a label on an item. - [Labels API](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels) — the same door over REST. --- # create_work_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-work-item Adds a work item of any kind to a project's backlog, with everything set at once; the full form beside create_story_draft. Add a work item: `project_id` and `title` are required; `type` is `story` by default or one of `task`, `subtask`, `bug`, `spike`, `improvement`; `description` (HTML), `epic_id`, `parent_id` (the story or task a sub-task or bug hangs under), `priority` (`medium` by default), `status` (`backlog` by default; later moves go through `move_work_item`), the four people, `story_points`, `estimate_minutes`, `remaining_minutes`, `due_on`, `component_ids` and `label_ids` apply to every kind. A story adds `persona_id`, `as_a`, `i_want`, `so_that`, `business_value_points` and `acceptance_criteria`; a bug adds `severity`, `environment_id`, `component_id`, `steps_to_reproduce`, `expected_result`, `actual_result` and `reproducible`; a spike adds `timebox_minutes` and `outcome`. The item takes the next number of the project, writes its first version and is announced as `work_item.created`. `create_story_draft` remains the short road for a story; use this for a task, a bug or a spike, or for a story with the persona, the labels and the components set in one call. A staff token with `work-item:create` on the project is needed. - Requires ability: `work-item:create` - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `title` | string | yes | The title, 3 to 200 characters. | | `type` | `story`, `task`, `subtask`, `bug`, `spike`, `improvement` | no | The kind; story by default. | | `status` | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` | no | Where it starts; backlog by default. Later moves go through move_work_item. | | `description` | string | no | What the item is, as HTML. | | `epic_id` | string | no | UUID of the epic it hangs under. | | `sprint_id` | string | no | UUID of the planned or running sprint it is planned into; null for the backlog. | | `parent_id` | string | no | UUID of the story or task a sub-task or bug hangs under. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | How urgent it is; medium by default. | | `assignee_id` | string | no | UUID of the person doing the work. | | `reporter_id` | string | no | UUID of the person who raised it; the acting person when left out on a create. | | `reviewer_id` | string | no | UUID of the person who reviews the code. | | `qa_owner_id` | string | no | UUID of the person who verifies it. | | `story_points` | string | no | The estimate on the project's scale, as text such as 5 or M. | | `estimate_minutes` | integer | no | The estimate in minutes. | | `remaining_minutes` | integer | no | What is left, in minutes. | | `due_on` | string | no | When it is due, YYYY-MM-DD. | | `persona_id` | string | no | UUID of the persona a story is written as. | | `as_a` | string | no | The persona of a story, as in "returning shopper". | | `i_want` | string | no | What they want, as in "to reorder my last basket". | | `so_that` | string | no | Why, as in "I do not pick every item again". | | `business_value_points` | integer | no | The value of a story, in points. | | `severity` | `blocker`, `critical`, `major`, `minor`, `trivial` | no | How badly a bug hurts. | | `environment_id` | string | no | UUID of the environment a bug was seen in. | | `component_id` | string | no | UUID of the component a bug was found in. | | `steps_to_reproduce` | string | no | How to make a bug happen, as HTML. | | `expected_result` | string | no | What should have happened, as HTML. | | `actual_result` | string | no | What happened, as HTML. | | `reproducible` | `always`, `sometimes`, `once`, `not_reproduced` | no | How reliably a bug comes back. | | `timebox_minutes` | integer | no | A spike's time box, in minutes. | | `outcome` | string | no | A spike's outcome, as HTML. | | `component_ids` | array of string | no | UUIDs of the components the item touches; the list replaces the current one. | | `label_ids` | array of string | no | UUIDs of the labels pinned on the item; the list replaces the current one. | | `acceptance_criteria` | array of any | no | The criteria that say when a story is done, replacing the list: objects with title (required), given, when, then and kind (functional, negative, edge or non_functional); a row with an id keeps that criterion. | ## Example call ```json { "name": "create_work_item", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "" } } ``` ## What it returns ```json { "data": { "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 7, "reference": "SCL-7", "type": "task", "parent_id": null, "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Set up the payment gateway sandbox", "description": "<p>Request sandbox keys and wire the SDK behind a feature flag.</p>", "status": "backlog", "resolution": null, "priority": "high", "assignee_id": null, "assignee_name": null, "reporter_id": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "reporter_name": "Bea Analyst", "reviewer_id": null, "qa_owner_id": null, "story_points": null, "estimate_minutes": 240, "remaining_minutes": null, "due_on": null, "started_at": null, "completed_at": null, "persona_id": null, "as_a": null, "i_want": null, "so_that": null, "business_value_points": null, "severity": null, "environment_id": null, "component_id": null, "steps_to_reproduce": null, "expected_result": null, "actual_result": null, "reproducible": null, "timebox_minutes": null, "outcome": null, "component_ids": ["c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b"], "labels": [ { "id": "5d6e7f8a-9b0c-4d1e-8f2a-3b4c5d6e7f8a", "name": "payments", "color": "emerald" } ], "acceptance_criteria": [], "position": 7, "version": 1, "approved_at": null, "created_at": "2026-10-28T10:05:00+00:00", "updated_at": "2026-10-28T10:05:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — title missing or outside 3 to 200 characters, a type, priority, status, severity or reproducible outside its set, an epic_id, parent_id, persona_id, component, environment or label that is not the project's, a parent the kind does not allow, a person who is not on the team, or a criterion without a title. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry work-item:create. - `TOKEN_MISSING_ABILITY` — token lacks work-item:create. ## Example prompts > "Add a task under the payments epic to set up the gateway sandbox, four hours, high priority." > "Raise a bug on staging: the order total ignores the discount; severity major." > "Add a two-day spike to compare SMS providers for order notifications." ## Related - [create_story_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-story-draft) — the short road for a story. - [update_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-work-item) — change it later. - [move_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — move it along its lifecycle. - [Work items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) — the same door over REST. --- # delete_label Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/delete-label Deletes a label of a project after the person confirms; every item wearing it loses it. Delete a label by `label_id`. Every item wearing it loses it. The removal goes through the same action the backlog's label manager runs and `label.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. A staff token with `work-item:update` on the project is needed. - Requires ability: `work-item:update` - Runs the same action as [`DELETE /v1/labels/{label}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/remove-a-label) - Fires events: [`label.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `label_id` | string | yes | UUID of the label to delete. | ## Example call ```json { "name": "delete_label", "arguments": { "label_id": "0e5a939b-1cb1-4100-a6f9-215defc8f604" } } ``` ## What it returns ```json { "data": { "label_id": "5d6e7f8a-9b0c-4d1e-8f2a-3b4c5d6e7f8a", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown label_id, or a label outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry work-item:update. - `TOKEN_MISSING_ABILITY` — token lacks work-item:update. ## Example prompts > "Delete the tech-debt label; we track that as improvements now." > "Remove the duplicate payments label." ## Related - [update_label](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-label) — rename it instead. - [list_labels](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-labels) — what is left. - [Labels API](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels) — the same door over REST. --- # delete_work_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/delete-work-item Deletes a work item after the person confirms; cancelling with a reason keeps it on the record instead. Delete a work item by `work_item_id`, with its criteria. Its reference is never reused and its sub-tasks stay, without a parent. The removal goes through the same action the Backlog page runs and `work_item.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer `move_work_item` to `cancelled` with a reason, so the record keeps the item and why it went. A staff token with `work-item:delete` on the project is needed. - Requires ability: `work-item:delete` - Runs the same action as [`DELETE /v1/work-items/{item}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/delete-a-work-item) - Fires events: [`work_item.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `work_item_id` | string | yes | UUID of the work item to delete. | ## Example call ```json { "name": "delete_work_item", "arguments": { "work_item_id": "656556ee-9249-4800-a4ad-d0a9546c9122" } } ``` ## What it returns ```json { "data": { "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown work_item_id, or an item outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry work-item:delete. - `TOKEN_MISSING_ABILITY` — token lacks work-item:delete. ## Example prompts > "Delete the duplicate story SCL-14." > "Remove the test task I added by mistake." ## Related - [move_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — cancel it with a reason instead. - [list_work_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items) — what is left. - [Work items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) — the same door over REST. --- # get_board_snapshot Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-board-snapshot Reads a project's board as the Board page draws it, the columns the project keeps in order with their limits, tallies and cards, for the running sprint, a sprint by id, every open item or the unplanned ones; a guest token reads none. Read the board before you pick up a card, so you know what the team committed to, which columns have room and what nobody holds. The answer carries `scope` (its `kind`: `sprint`, `all` or `unplanned`, and for a sprint its `sprint_id`, `sprint_name`, `sprint_status`, `ends_on` and `days_left`) and `columns` in the board's order, each with its `status` and `label`, its `wip_limit` (null when the project does not cap it), `count`, `full` (true when the column holds its limit or more, so a move into it is refused), `points` and its `items` in the board's order: `id`, `reference`, `title`, `type`, `status`, `priority`, `story_points`, `assignee_id`, `assignee_name`, `epic_id`, `epic_reference`, `sprint_id` and `position`. The columns the project hides are left out. The default `scope` is `active`, the running sprint, or every open item when no sprint runs; `all` adds the items finished or dropped in the last two weeks; `unplanned` is the items in no sprint; a sprint's UUID reads that sprint. `assignee_id` and `search` narrow the cards, never the tallies. To move a card use `move_work_item`, which the same limits and lifecycle gate; to take one use `assign_work_item`. A client guest's token reads no board. - Requires ability: `work-item:view-any` - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `scope` | string | no | What the board shows: active (the running sprint; the default), all, unplanned, or a sprint UUID. | | `assignee_id` | string | no | Narrow the cards to one person, by user UUID. | | `search` | string | no | Words the reference or the title must contain. | ## Example call ```json { "name": "get_board_snapshot", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "scope": { "kind": "sprint", "sprint_id": "8b3f6a02-2d5c-4e9f-8c4b-7d8e9f0a1b22", "sprint_name": "Sprint 3", "sprint_status": "active", "ends_on": "2026-11-22", "days_left": 9 }, "columns": [ { "status": "backlog", "label": "Backlog", "wip_limit": null, "count": 0, "full": false, "points": 0, "items": [] }, { "status": "ready", "label": "Ready", "wip_limit": null, "count": 1, "full": false, "points": 5, "items": [ { "id": "4f1c2d3e-5a6b-4c7d-8e9f-0a1b2c3d4e5f", "reference": "CRM-142", "title": "Reset a forgotten password", "type": "story", "status": "ready", "priority": "high", "story_points": "5", "assignee_id": null, "assignee_name": null, "epic_id": "2c1b0a9f-8e7d-4c6b-a5f4-3e2d1c0b9a88", "epic_reference": "CRM-12", "sprint_id": "8b3f6a02-2d5c-4e9f-8c4b-7d8e9f0a1b22", "position": 1000 } ] }, { "status": "in_progress", "label": "In progress", "wip_limit": 2, "count": 2, "full": true, "points": 8, "items": [ { "id": "5a2d3e4f-6b7c-4d8e-9f0a-1b2c3d4e5f60", "reference": "CRM-143", "title": "See my orders", "type": "story", "status": "in_progress", "priority": "medium", "story_points": "3", "assignee_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "assignee_name": "Pat Manager", "epic_id": "2c1b0a9f-8e7d-4c6b-a5f4-3e2d1c0b9a88", "epic_reference": "CRM-12", "sprint_id": "8b3f6a02-2d5c-4e9f-8c4b-7d8e9f0a1b22", "position": 2000 }, { "id": "6b3e4f50-7c8d-4e9f-8a1b-2c3d4e5f6a71", "reference": "CRM-147", "title": "Export my invoices", "type": "bug", "status": "in_progress", "priority": "high", "story_points": "5", "assignee_id": null, "assignee_name": null, "epic_id": null, "epic_reference": null, "sprint_id": "8b3f6a02-2d5c-4e9f-8c4b-7d8e9f0a1b22", "position": 3000 } ] }, { "status": "in_review", "label": "In review", "wip_limit": null, "count": 0, "full": false, "points": 0, "items": [] }, { "status": "in_qa", "label": "In QA", "wip_limit": null, "count": 0, "full": false, "points": 0, "items": [] }, { "status": "done", "label": "Done", "wip_limit": null, "count": 0, "full": false, "points": 0, "items": [] } ], "item_count": 3 } } ``` ## How it fails - `VALIDATION_FAILED` — scope is not active, all, unplanned or a UUID. - `RESOURCE_NOT_FOUND` — unknown project_id, a project outside the token's scope, or a scope UUID that is not one of the project's sprints. - `FORBIDDEN` — the holder's role on the project does not carry work-item:view-any, or the token is a client guest's. - `TOKEN_MISSING_ABILITY` — token lacks work-item:view-any. ## Example prompts > "What is on the board for the running sprint of the Customer Portal, and which columns are full?" > "Which cards in progress have nobody on them? Show me the unplanned work too." > "Read the board for sprint `8b3f6a02-2d5c-4e9f-8c4b-7d8e9f0a1b22` and list what is ready to pick up, in order." ## Related - [move_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — move a card to another column, under the same lifecycle and limits. - [assign_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/assign-work-item) — take a card or hand it to someone. - [get_sprint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-sprint) — the sprint the board follows, with its points and dates. - [list_work_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items) — the same items as a list, with every filter. - [Work items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) — the rank endpoint that changes a card's place. --- # get_sprint Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-sprint Fetches one sprint by id with everything the list gives and its scope, the work items it holds in the backlog's order with their status and points, plus the points planned and done. Read one sprint in full before you pick up or add work. Beside the row `list_sprints` gives, the answer carries `items`: every work item the sprint holds, in the backlog's order, each with `id`, `reference`, `title`, `type`, `status`, `priority`, `story_points` and `assignee_id`; and `points_planned` and `points_done`, the points of all its items and of the ones done so far, so you can tell how a running sprint stands against the points it committed to. To move an item into or out of the sprint, change the item's own `sprint_id` with `update_work_item`; a sprint that is with the team for its commitment, completed or cancelled takes no more items. A client guest's token reads no sprints, so a sprint it asks for is unknown. - Requires ability: `sprint:view` - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `sprint_id` | string | yes | UUID of the sprint to fetch. | ## Example call ```json { "name": "get_sprint", "arguments": { "sprint_id": "219612c3-8986-4400-a405-e9486d6d261e" } } ``` ## What it returns ```json { "data": { "id": "8b3f6a02-2d5c-4e9f-8c4b-7d8e9f0a1b22", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 3, "name": "Sprint 3", "goal": "<p>Customers reset their password and see their orders.</p>", "starts_on": "2026-11-09", "ends_on": "2026-11-22", "duration_days": 14, "days_left": 9, "status": "active", "capacity_points": 21, "capacity_hours": 160, "committed_points": 21, "completed_points": null, "velocity": null, "retrospective": null, "item_count": 2, "points_planned": 8, "points_done": 5, "items": [ { "id": "4f1c2d3e-5a6b-4c7d-8e9f-0a1b2c3d4e5f", "reference": "CRM-142", "title": "Reset a forgotten password", "type": "story", "status": "done", "priority": "high", "story_points": "5", "assignee_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b" }, { "id": "5a2d3e4f-6b7c-4d8e-9f0a-1b2c3d4e5f60", "reference": "CRM-143", "title": "See my orders", "type": "story", "status": "in_progress", "priority": "medium", "story_points": "3", "assignee_id": null } ], "started_by": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "starter_name": "Pat Manager", "started_at": "2026-11-09T09:00:00+00:00", "closed_by": null, "closer_name": null, "closed_at": null, "version": 1, "approved_at": "2026-11-09T09:00:00+00:00", "approved_revision_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "created_at": "2026-11-03T08:00:00+00:00", "updated_at": "2026-11-09T09:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown sprint_id, a sprint of a project outside the token's scope, or a client guest's token. - `FORBIDDEN` — the holder's role on the project does not carry sprint:view. - `TOKEN_MISSING_ABILITY` — token lacks sprint:view. ## Example prompts > "What is in the running sprint of the Customer Portal, and what is still open?" > "Read sprint `8b3f6a02-2d5c-4e9f-8c4b-7d8e9f0a1b22` and tell me how many points are done against the commitment." > "Which stories in Sprint 3 have nobody on them?" ## Related - [list_sprints](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-sprints) — the project's sprints and which one runs. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — one item of the scope with its acceptance criteria. - [update_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-work-item) — plan an item into the sprint or send it back to the backlog. - [Sprints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/sprints) — the same read over REST. --- # get_work_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item One work item by id, with its reference, kind, title, prose, status, priority, people, estimates, the fields its kind adds, the components it touches and its acceptance criteria in order. Return one work item: its reference, kind, title, description, status and resolution, priority, the people on it, its estimates and dates, the fields its kind adds (a story's persona and sentence, a bug's severity, environment, reproduction and results, a spike's time box and outcome), the ids of the components it touches and its acceptance criteria in order, each with its kind and its Given, When and Then. Read it before you implement, test or review an item, so the criteria and the expected behaviour are in front of you, and read the epic it hangs under with `get_epic` for the goal. An item of a project the token may not see, or an item that is not agreed read through a guest token, is unknown rather than forbidden. - Requires ability: `work-item:view` - Runs the same action as [`GET /v1/work-items/{item}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-work-item) - Runs the same action as [`GET /v1/work-items/{item}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-versions) - Runs the same action as [`GET /v1/work-items/{item}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/get-a-version-of-a-work-item) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `work_item_id` | string | yes | UUID of the work item to fetch. | ## Example call ```json { "name": "get_work_item", "arguments": { "work_item_id": "656556ee-9249-4800-a4ad-d0a9546c9122" } } ``` ## What it returns ```json { "data": { "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 142, "reference": "CRM-142", "type": "story", "parent_id": null, "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Tier upgrade email", "description": "<p>Tell a member they reached the next tier the day it happens.</p>", "status": "in_progress", "resolution": null, "priority": "high", "assignee_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "assignee_name": "Nimal Fernando", "reporter_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "reporter_name": "Dilani Perera", "reviewer_id": null, "qa_owner_id": null, "story_points": "5", "estimate_minutes": null, "remaining_minutes": null, "due_on": "2026-11-20", "started_at": "2026-11-02T09:00:00+00:00", "completed_at": null, "persona_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "as_a": "gold member", "i_want": "to be told the day I reach platinum", "so_that": "I use the new perks straight away", "business_value_points": 8, "severity": null, "environment_id": null, "component_id": null, "steps_to_reproduce": null, "expected_result": null, "actual_result": null, "reproducible": null, "timebox_minutes": null, "outcome": null, "component_ids": ["f6a7b8c9-d0e1-4f2a-8b3c-4d5e6f7a8b92"], "acceptance_criteria": [ { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c03", "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "position": 0, "title": "A member who crosses the platinum threshold gets the email within the hour", "given": "a gold member whose annual spend crosses the platinum threshold", "when": "the nightly tier job runs", "then": "the tier upgrade email is sent within the hour", "kind": "functional", "verified_at": null, "verified_by": null } ], "position": 3, "version": 0, "approved_at": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-02T09:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown work_item_id, an item of a project outside the token's scope or the person's visibility, or an item that is not agreed read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks work-item:view. ## Example prompts > "Read CRM-142 and list its acceptance criteria." > "What are the steps to reproduce the receipt totals bug, and in which environment was it seen?" > "Is item `b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e` still in progress, and who reviews it?" ## Related - [list_work_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items) - [get_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-epic) — the goal the item serves. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — every version of the item, criteria included. - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) --- # Build Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build The tracker of a project: its stories, tasks, sub-tasks, bugs, spikes and improvements, each numbered from the same sequence as the epics (`CRM-142`) with its kind, status from the backlog to done, resolution, priority, the people on it, its estimates and dates, the fields its kind adds and, for a story, the acceptance criteria that say when it is done. These tools read a project's backlog and what is assigned to the person the token acts as (a guest token the agreed items only), read one item with its criteria before it is implemented or tested, move an item along its lifecycle with a resolution on a close, and hand it to a member of the team; create an item of any kind with everything set at once (`create_work_item`), change one in part with its criteria, components and labels (`update_work_item`) and delete one after a confirmation; the move and the hand-over are destructive in the MCP sense, so an agent confirms before moving or taking work that is not the person's. The labels are the project's own words pinned on its items, each with a colour and a meaning; an agent reads them (`list_labels`), filters the backlog by one, and coins, renames and removes them (`create_label`, `update_label`, `delete_label`) under the same permission that changes an item. The sprints the team plans and commits to are read here too (`list_sprints`: the running sprint, the planned ones and the closed ones with their velocity; `get_sprint`: one sprint with the items it holds and its points), and an item is planned into a sprint through its own `sprint_id` on `update_work_item`; planning, starting, closing and cancelling a sprint stay in the application and the REST API. The board is read here too (`get_board_snapshot`: the columns the project keeps in order with their limits, tallies and cards, for the running sprint, a sprint by id, every open item or the unplanned ones), and a card's column changes through `move_work_item` under the same lifecycle and limits; ranking a card stays in the application and the REST API. Time logs and code links join as each module lands. ## Tools - [`assign_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/assign-work-item) — Assign Work Item (destructive) - [`create_label`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-label) — Create Label (destructive) - [`create_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-work-item) — Create Work Item (destructive) - [`delete_label`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/delete-label) — Delete Label (destructive) - [`delete_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/delete-work-item) — Delete Work Item (destructive) - [`get_board_snapshot`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-board-snapshot) — Get Board Snapshot (read) - [`get_sprint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-sprint) — Get Sprint (read) - [`get_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — Get Work Item (read) - [`list_labels`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-labels) — List Labels (read) - [`list_my_work`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-my-work) — List My Work (read) - [`list_sprints`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-sprints) — List Sprints (read) - [`list_work_items`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items) — List Work Items (read) - [`move_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — Move Work Item (destructive) - [`update_label`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-label) — Update Label (destructive) - [`update_work_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-work-item) — Update Work Item (destructive) --- # list_labels Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-labels A project's labels by name, each with its colour, what it means and how many work items carry it; the vocabulary to read before you draft or filter stories. Return a project's labels by name: the words the team pins on its work items, each with its colour, what it means and how many work items carry it. Read it to learn a project's own vocabulary before you read or draft its stories, to describe the backlog in the team's words, and to name a label by id when you filter work items with `label_id`. Adding, renaming and removing words stays with people. - Requires ability: `work-item:view-any` - Runs the same action as [`GET /v1/projects/{project}/labels`](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/list-a-projects-labels) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose labels to list. | ## Example call ```json { "name": "list_labels", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "payments", "color": "amber", "description": "Touches the checkout or the ledger", "work_item_count": 7, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-10-28T09:20:00+00:00" }, { "id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d12", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "tech-debt", "color": "violet", "description": "Costs us time every sprint", "work_item_count": 3, "created_at": "2026-10-28T09:25:00+00:00", "updated_at": "2026-10-28T09:25:00+00:00" } ] } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token holder's role on the project does not carry work-item:view-any. - `TOKEN_MISSING_ABILITY` — token lacks work-item:view-any. ## Example prompts > "What labels does the Customer Portal project use, and which is the most common?" > "List the stories labelled tech-debt in project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`." > "Which label should a story about the voucher ledger carry?" ## Related - [list_work_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items) — filter the backlog with label_id. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — an item with its labels. - [Labels API](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels) --- # list_my_work Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-my-work The work items assigned to the person the token acts as, across every project the token may see, open ones by default. Return the work items assigned to the person you act as, across every project the token may see, newest change first: each with its reference, project, kind, title, status, priority, estimates and dates. Read it at the start of a session to know what is on the person's plate, or when they ask what is next; the open items (neither done nor cancelled) come back by default and `include_closed` adds the done and cancelled ones. The list spans projects, so read `project_id` and `reference` together when you report, and read the item with `get_work_item` before you implement it. A guest token reads the agreed items only, which is none until an approval pins them. - Requires ability: `work-item:view-any` - Runs the same action as [`GET /v1/me/work-items`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-my-work-items) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `include_closed` | boolean | no | True to include the done and cancelled items too. | | `limit` | integer | no | Items per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_my_work", "arguments": { "include_closed": true, "limit": 1 } } ``` ## What it returns ```json { "data": [ { "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 142, "reference": "CRM-142", "type": "story", "parent_id": null, "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Tier upgrade email", "description": null, "status": "ready", "resolution": null, "priority": "high", "assignee_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "assignee_name": "Nimal Fernando", "reporter_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "reporter_name": "Dilani Perera", "reviewer_id": null, "qa_owner_id": null, "story_points": "5", "estimate_minutes": null, "remaining_minutes": null, "due_on": "2026-11-20", "started_at": null, "completed_at": null, "persona_id": null, "as_a": "gold member", "i_want": "to be told the day I reach platinum", "so_that": "I use the new perks straight away", "business_value_points": null, "severity": null, "environment_id": null, "component_id": null, "steps_to_reproduce": null, "expected_result": null, "actual_result": null, "reproducible": null, "timebox_minutes": null, "outcome": null, "component_ids": [], "acceptance_criteria": [], "position": 3, "version": 0, "approved_at": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-01T16:30:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `TOKEN_MISSING_ABILITY` — token lacks work-item:view-any. ## Example prompts > "What is on my plate right now?" > "Which of my items are due this week?" > "Show me everything I finished this month, done and cancelled included." ## Related - [list_work_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items) — a whole project's backlog. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — the item with its criteria, before you start. - [move_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — move it along as you work. - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) --- # list_sprints Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-sprints Lists a project's sprints in number order with their dates, status, capacity, the points committed to and finished, the velocity, the item count and who started and closed them, narrowed by status, open or words; a guest token reads none. Read the sprints of a project before you pick up or plan work. Each row carries the `number` and `name`, the `goal`, `starts_on`, `ends_on`, `duration_days` and `days_left`, the `status` (`planned`, `committing` while the team votes, `active` once it runs, `completed` or `cancelled`), `capacity_points` and `capacity_hours`, `committed_points` (the scope's points when the team committed), `completed_points` and `velocity` once closed, the `retrospective`, `item_count`, who started it and when, who closed it and when, and the commitment's approval stamps. Narrow with `status`, with `open` true for the sprints still ahead or running, or with `search` (words the name must contain); page with `limit` and `page`. The running sprint is the one with `status` `active`, and there is at most one; read its scope with `get_sprint`. A client guest's token reads no sprints. - Requires ability: `sprint:view-any` - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `status` | `planned`, `committing`, `active`, `completed`, `cancelled` | no | Narrow to one status. | | `open` | boolean | no | True for the sprints that are planned, awaiting commitment or running. | | `search` | string | no | Words the name must contain. | | `limit` | integer | no | Rows per page, 1 to 100; 25 by default. | | `page` | integer | no | The page, from 1. | ## Example call ```json { "name": "list_sprints", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "8b3f6a02-2d5c-4e9f-8c4b-7d8e9f0a1b22", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 3, "name": "Sprint 3", "goal": "<p>Customers reset their password and see their orders.</p>", "starts_on": "2026-11-09", "ends_on": "2026-11-22", "duration_days": 14, "days_left": 9, "status": "active", "capacity_points": 21, "capacity_hours": 160, "committed_points": 21, "completed_points": null, "velocity": null, "retrospective": null, "item_count": 8, "started_by": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "starter_name": "Pat Manager", "started_at": "2026-11-09T09:00:00+00:00", "closed_by": null, "closer_name": null, "closed_at": null, "version": 1, "approved_at": "2026-11-09T09:00:00+00:00", "approved_revision_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "created_at": "2026-11-03T08:00:00+00:00", "updated_at": "2026-11-09T09:00:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 3, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — unknown status (the response lists the supported values), or limit outside 1..100. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry sprint:view-any. - `TOKEN_MISSING_ABILITY` — token lacks sprint:view-any. ## Example prompts > "Which sprint of the Customer Portal is running, and how many days are left?" > "List the planned sprints of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` so I can plan the password story into the next one." > "What velocity did the last three sprints reach?" ## Related - [get_sprint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-sprint) — one sprint with the work items it holds and its points. - [list_work_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items) — the backlog, narrowed to one sprint with sprint_id or to the unplanned items with unplanned. - [update_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-work-item) — plan an item into a sprint through its sprint_id. - [Sprints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/sprints) — the same list over REST, and the writes. --- # list_work_items Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-work-items A project's stories, tasks, sub-tasks, bugs, spikes and improvements in the project's order, with reference, kind, status, priority, people and estimate, narrowed by the typed filters and for a guest token. Return the work items of one project, the rows of its tracker, each with its reference (such as `CRM-142`), kind, title, status, resolution, priority, the people on it (with the assignee's and the reporter's names), its estimates and dates, the fields its kind adds and the ids of the components it touches. Read it to find the work before you pick it up, to see what hangs under an epic or a parent, or to report where a project stands; name items by their reference when you do. Optional filters narrow the page to one `type`, one `status`, one `priority`, the items under one `epic_id` or one `parent_id`, the items one `assignee_id` is doing, the `open` items (neither done nor cancelled) or the items whose reference or title contain `search`. A guest token reads the agreed items only. The acceptance criteria are not in the list; `get_work_item` reads them. - Requires ability: `work-item:view-any` - Runs the same action as [`GET /v1/projects/{project}/work-items`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-work-items) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose items to list. | | `type` | `story`, `task`, `subtask`, `bug`, `spike`, `improvement` | no | Narrow to one kind. | | `status` | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` | no | Narrow to one status. | | `open` | boolean | no | True for the items that are neither done nor cancelled. | | `epic_id` | string | no | Narrow to the items under one epic, by UUID. | | `sprint_id` | string | no | Narrow to the items planned into one sprint, by UUID. | | `unplanned` | boolean | no | True for the items in no sprint. | | `parent_id` | string | no | Narrow to the items under one parent item, by UUID. | | `assignee_id` | string | no | Narrow to the items one person is doing, by user UUID. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | Narrow to one priority. | | `search` | string | no | Words the reference or the title must contain. | | `limit` | integer | no | Items per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_work_items", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 142, "reference": "CRM-142", "type": "story", "parent_id": null, "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Tier upgrade email", "description": "<p>Tell a member they reached the next tier the day it happens.</p>", "status": "in_progress", "resolution": null, "priority": "high", "assignee_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "assignee_name": "Nimal Fernando", "reporter_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "reporter_name": "Dilani Perera", "reviewer_id": null, "qa_owner_id": null, "story_points": "5", "estimate_minutes": null, "remaining_minutes": null, "due_on": "2026-11-20", "started_at": "2026-11-02T09:00:00+00:00", "completed_at": null, "persona_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "as_a": "gold member", "i_want": "to be told the day I reach platinum", "so_that": "I use the new perks straight away", "business_value_points": 8, "severity": null, "environment_id": null, "component_id": null, "steps_to_reproduce": null, "expected_result": null, "actual_result": null, "reproducible": null, "timebox_minutes": null, "outcome": null, "component_ids": ["f6a7b8c9-d0e1-4f2a-8b3c-4d5e6f7a8b92"], "acceptance_criteria": [], "position": 3, "version": 0, "approved_at": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-02T09:00:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — type, status or priority is not one of its values, or an id filter is not a UUID. - `TOKEN_MISSING_ABILITY` — token lacks work-item:view-any. ## Example prompts > "What is open in the customer portal backlog, and who holds each item?" > "List the bugs of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` that are still in progress." > "Which stories hang under the loyalty tiers epic and which of them are ready to pick up?" ## Related - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — one item with its criteria. - [list_my_work](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-my-work) — what is assigned to the person you act as. - [list_epics](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-epics) — the epics the items hang under. - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) --- # move_work_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item Move a work item along its lifecycle, from the backlog through ready, in progress, in review and in QA to done or cancelled, with a resolution on a close. Move a work item to a new status, following the moves the lifecycle allows from where it stands: from the backlog to ready, in progress or cancelled; from ready back to the backlog, on to in progress or cancelled; from in progress to ready, in review, in QA, done or cancelled; from in review to in progress, in QA, done or cancelled; from in QA to in progress, done or cancelled; a done item back to in progress; a cancelled one back to the backlog. A move into `done` resolves the item as done; a move into `cancelled` takes `wont_do` (the default), `duplicate`, `cannot_reproduce` or `deferred`. The first move into progress stamps when work started, a move into done stamps the finish, and the item writes a version with the reason `transition`; `work_item.transitioned` goes out with the status before and after. Destructive in the MCP sense: it changes where someone's work stands, so read the item with `get_work_item` first and confirm with the person before moving an item that is not theirs. Say why in your reply; the comment that will travel with a move is not here yet. A move to the status the item is in already changes nothing. - Requires ability: `work-item:transition` - Runs the same action as [`POST /v1/work-items/{item}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/move-a-work-item) - Fires events: [`work_item.transitioned`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-transitioned) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `work_item_id` | string | yes | UUID of the work item to move. | | `status` | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` | yes | Where the item goes. | | `resolution` | `done`, `wont_do`, `duplicate`, `cannot_reproduce`, `deferred` | no | Why it closes, for done or cancelled. | ## Example call ```json { "name": "move_work_item", "arguments": { "work_item_id": "656556ee-9249-4800-a4ad-d0a9546c9122", "status": "backlog" } } ``` ## What it returns ```json { "data": { "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 142, "reference": "CRM-142", "type": "story", "parent_id": null, "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Tier upgrade email", "description": null, "status": "in_review", "resolution": null, "priority": "high", "assignee_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "assignee_name": "Nimal Fernando", "reporter_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "reporter_name": "Dilani Perera", "reviewer_id": null, "qa_owner_id": null, "story_points": "5", "estimate_minutes": null, "remaining_minutes": null, "due_on": "2026-11-20", "started_at": "2026-11-02T09:00:00+00:00", "completed_at": null, "persona_id": null, "as_a": "gold member", "i_want": "to be told the day I reach platinum", "so_that": "I use the new perks straight away", "business_value_points": null, "severity": null, "environment_id": null, "component_id": null, "steps_to_reproduce": null, "expected_result": null, "actual_result": null, "reproducible": null, "timebox_minutes": null, "outcome": null, "component_ids": [], "acceptance_criteria": [], "position": 3, "version": 0, "approved_at": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-04T14:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown work_item_id, or an item outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — status is unknown, the lifecycle does not allow the move from where the item stands, or the resolution does not fit the status (done takes done; cancelled takes any other). - `TOKEN_MISSING_ABILITY` — token lacks work-item:transition. - `FORBIDDEN` — the token holder's role on the project does not carry work-item:transition. ## Example prompts > "I have opened the pull request for CRM-142; move it to in review." > "Cancel the receipt totals bug as a duplicate of CRM-139." > "Move item `b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e` back to in progress; QA found a gap." ## Related - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — read where it stands first. - [assign_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/assign-work-item) — hand it to the next person. - [list_my_work](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-my-work) - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) --- # update_label Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-label Changes a label in part; every item wearing it shows the new word at once. Change a label by `label_id` with any of `name`, `color` and `description`. A field left out keeps its value. The change goes through the same action the backlog's label manager runs and `label.updated` goes out. A staff token with `work-item:update` on the project is needed. - Requires ability: `work-item:update` - Runs the same action as [`PATCH /v1/labels/{label}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels/rewrite-a-label) - Fires events: [`label.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `label_id` | string | yes | UUID of the label. | | `name` | string | no | The word, 1 to 60 characters, unique within the project. | | `color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` | no | One of the palette's colours. | | `description` | string | no | What the label means, up to 160 characters. | ## Example call ```json { "name": "update_label", "arguments": { "label_id": "0e5a939b-1cb1-4100-a6f9-215defc8f604" } } ``` ## What it returns ```json { "data": { "id": "5d6e7f8a-9b0c-4d1e-8f2a-3b4c5d6e7f8a", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "payments", "color": "emerald", "description": "Anything that moves money, including refunds", "work_item_count": 6, "created_at": "2026-10-28T09:50:00+00:00", "updated_at": "2026-10-30T08:30:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a name another label of the project uses, a color outside the palette, or a description over 160 characters. - `RESOURCE_NOT_FOUND` — unknown label_id, or a label outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry work-item:update. - `TOKEN_MISSING_ABILITY` — token lacks work-item:update. ## Example prompts > "Rename the logistics label to pickup-and-delivery." > "Make the urgent label red and say it means blocking the launch." ## Related - [list_labels](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/list-labels) — the labels with their counts. - [delete_label](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/delete-label) — take a word away. - [Labels API](https://alm-docs.labs.zuselab.dev/api/v1/reference/labels) — the same door over REST. --- # update_work_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/update-work-item Changes a work item in part; the criteria, components and labels are replaced only when their list is sent, and every change writes a version. Change a work item by `work_item_id` with any of `title`, `type`, `description` (HTML), `epic_id`, `parent_id`, `priority`, the four people, `story_points`, `estimate_minutes`, `remaining_minutes`, `due_on`, `component_ids` and `label_ids` (each replaces its list), the story fields `persona_id`, `as_a`, `i_want`, `so_that`, `business_value_points` and `acceptance_criteria` (replaces the list; a row with an `id` keeps and changes that criterion), the bug fields `severity`, `environment_id`, `component_id`, `steps_to_reproduce`, `expected_result`, `actual_result`, `reproducible`, and the spike fields `timebox_minutes` and `outcome`. A field left out keeps its value; a field sent as null is cleared. Changing the `type` drops the fields the new kind does not carry. The change goes through the same action the Backlog page runs, writes a version a person can put back, and `work_item.updated` goes out. The status moves through `move_work_item` and the assignee may also move through `assign_work_item`. A staff token with `work-item:update` on the project is needed. - Requires ability: `work-item:update` - Runs the same action as [`PATCH /v1/work-items/{item}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/update-a-work-item) - Fires events: [`work_item.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `work_item_id` | string | yes | UUID of the work item. | | `title` | string | no | The title, 3 to 200 characters. | | `type` | `story`, `task`, `subtask`, `bug`, `spike`, `improvement` | no | The kind; changing it drops the fields the new kind does not carry. | | `description` | string | no | What the item is, as HTML. | | `epic_id` | string | no | UUID of the epic it hangs under. | | `sprint_id` | string | no | UUID of the planned or running sprint it is planned into; null for the backlog. | | `parent_id` | string | no | UUID of the story or task a sub-task or bug hangs under. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | How urgent it is; medium by default. | | `assignee_id` | string | no | UUID of the person doing the work. | | `reporter_id` | string | no | UUID of the person who raised it; the acting person when left out on a create. | | `reviewer_id` | string | no | UUID of the person who reviews the code. | | `qa_owner_id` | string | no | UUID of the person who verifies it. | | `story_points` | string | no | The estimate on the project's scale, as text such as 5 or M. | | `estimate_minutes` | integer | no | The estimate in minutes. | | `remaining_minutes` | integer | no | What is left, in minutes. | | `due_on` | string | no | When it is due, YYYY-MM-DD. | | `persona_id` | string | no | UUID of the persona a story is written as. | | `as_a` | string | no | The persona of a story, as in "returning shopper". | | `i_want` | string | no | What they want, as in "to reorder my last basket". | | `so_that` | string | no | Why, as in "I do not pick every item again". | | `business_value_points` | integer | no | The value of a story, in points. | | `severity` | `blocker`, `critical`, `major`, `minor`, `trivial` | no | How badly a bug hurts. | | `environment_id` | string | no | UUID of the environment a bug was seen in. | | `component_id` | string | no | UUID of the component a bug was found in. | | `steps_to_reproduce` | string | no | How to make a bug happen, as HTML. | | `expected_result` | string | no | What should have happened, as HTML. | | `actual_result` | string | no | What happened, as HTML. | | `reproducible` | `always`, `sometimes`, `once`, `not_reproduced` | no | How reliably a bug comes back. | | `timebox_minutes` | integer | no | A spike's time box, in minutes. | | `outcome` | string | no | A spike's outcome, as HTML. | | `component_ids` | array of string | no | UUIDs of the components the item touches; the list replaces the current one. | | `label_ids` | array of string | no | UUIDs of the labels pinned on the item; the list replaces the current one. | | `acceptance_criteria` | array of any | no | The criteria that say when a story is done, replacing the list: objects with title (required), given, when, then and kind (functional, negative, edge or non_functional); a row with an id keeps that criterion. | ## Example call ```json { "name": "update_work_item", "arguments": { "work_item_id": "656556ee-9249-4800-a4ad-d0a9546c9122" } } ``` ## What it returns ```json { "data": { "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 8, "reference": "SCL-8", "type": "story", "parent_id": null, "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Track my order from my phone", "description": null, "status": "backlog", "resolution": null, "priority": "medium", "assignee_id": null, "assignee_name": null, "reporter_id": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "reporter_name": "Bea Analyst", "reviewer_id": null, "qa_owner_id": null, "story_points": "3", "estimate_minutes": null, "remaining_minutes": null, "due_on": null, "started_at": null, "completed_at": null, "persona_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c", "as_a": "busy parent", "i_want": "to see where my laundry is", "so_that": "I know when to collect it", "business_value_points": null, "severity": null, "environment_id": null, "component_id": null, "steps_to_reproduce": null, "expected_result": null, "actual_result": null, "reproducible": null, "timebox_minutes": null, "outcome": null, "component_ids": [], "labels": [], "acceptance_criteria": [ { "id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "position": 1, "title": "The status page shows the current stage and the next one", "given": null, "when": null, "then": null, "kind": "functional", "verified_at": null, "verified_by": null } ], "position": 8, "version": 2, "approved_at": null, "created_at": "2026-10-28T10:08:00+00:00", "updated_at": "2026-10-28T10:20:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a value outside its set, an epic, parent, persona, component, environment or label that is not the project's, a parent the kind does not allow, a person who is not on the team, or a criterion without a title. - `RESOURCE_NOT_FOUND` — unknown work_item_id, or an item outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry work-item:update. - `TOKEN_MISSING_ABILITY` — token lacks work-item:update. ## Example prompts > "Rewrite the tracking story's criteria: the status page shows the current stage and the next one, and a cancelled order shows no stage." > "Size SCL-8 at three points and tag it with notifications." > "Turn SCL-12 into a bug; it is a defect, not a story." ## Related - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — the item as it stands. - [move_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — the status. - [assign_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/assign-work-item) — the person doing the work. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — every version the changes wrote. - [Work items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) — the same doors over REST. --- # create_repository Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/create-repository Registers where a component's code lives, so the Git integration can link branches, commits and pull requests to it later. Register a repository on a project: `project_id`, `owner` and `name` are required, spelled as the provider spells them; `provider` is `github` by default or `gitlab` or `bitbucket`; `url` is derived from the provider when left out; `default_branch` is `main` by default; `private` is true by default; `component_id` names the component it builds (omit it when it serves the whole project). The repository is written through the same action the Repositories page and the intake wizard run, and `repository.created` goes out. Registering records where the code lives; connecting the provider, which lets the integration read pushes and pull requests, is done in the application. A staff token with `repository:create` on the project is needed. - Requires ability: `repository:create` - Runs the same action as [`POST /v1/projects/{project}/repositories`](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/register-a-repository) - Fires events: [`repository.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `owner` | string | yes | The account or group that owns the repository, as the provider spells it. | | `name` | string | yes | The repository's name, as the provider spells it. | | `provider` | `github`, `gitlab`, `bitbucket` | no | Where the repository is hosted; github by default. | | `url` | string | no | Where it lives when not on the provider's own host. | | `default_branch` | string | no | The branch releases are cut from; main by default. | | `private` | boolean | no | Whether the repository is private; true by default. | | `component_id` | string | no | UUID of the component it builds; omit when it serves the whole project. | ## Example call ```json { "name": "create_repository", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "owner": "<owner>", "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "4c5d6e7f-8a9b-4c0d-8e1f-2a3b4c5d6e7f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "provider": "github", "owner": "zuse-technologies", "name": "laundry-web", "full_name": "zuse-technologies/laundry-web", "default_branch": "main", "url": "https://github.com/zuse-technologies/laundry-web", "private": true, "connected_by": null, "connected_at": null, "disconnected_at": null, "created_at": "2026-10-28T09:45:00+00:00", "updated_at": "2026-10-28T09:45:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — owner or name missing, a provider outside the three, a url that is not an address, a component_id that is not the project's, or a repository the project already registered. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry repository:create. - `TOKEN_MISSING_ABILITY` — token lacks repository:create. ## Example prompts > "Register zuse-technologies/laundry-web as the web app's repository." > "The API lives in GitLab under zuse/laundry-api; add it." > "Add a repository for the driver app on the develop branch." ## Related - [list_repositories](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/list-repositories) — the repositories registered today. - [update_repository](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/update-repository) — change one later. - [Repositories API](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories) — the same door over REST. --- # delete_repository Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/delete-repository Removes a repository from a project's registry after the person confirms; the code itself is untouched. Remove a repository by `repository_id`. The registry row goes; the code on the provider is untouched, and the integration stops linking to it. The removal goes through the same action the Repositories page runs and `repository.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. A staff token with `repository:delete` on the project is needed. - Requires ability: `repository:delete` - Runs the same action as [`DELETE /v1/repositories/{repository}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/delete-a-repository) - Fires events: [`repository.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `repository_id` | string | yes | UUID of the repository to remove. | ## Example call ```json { "name": "delete_repository", "arguments": { "repository_id": "79fb9e03-114c-4c00-a183-318468094cca" } } ``` ## What it returns ```json { "data": { "repository_id": "4c5d6e7f-8a9b-4c0d-8e1f-2a3b4c5d6e7f", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown repository_id, or a repository outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry repository:delete. - `TOKEN_MISSING_ABILITY` — token lacks repository:delete. ## Example prompts > "Remove the old prototype repository from the project." > "Delete the repository I registered under the wrong owner." ## Related - [update_repository](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/update-repository) — change it instead. - [list_repositories](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/list-repositories) — what is left. - [Repositories API](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories) — the same door over REST. --- # get_repository Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/get-repository One repository by id, with its provider, owner, name, default branch, address, privacy, component and disconnected stamp. Return one repository of a project: the provider, the owner and name as the provider spells them, the full name, the branch releases are cut from, the address to clone from, whether it is private, the component it builds, who registered it and whether it is set aside. Read it when a story, a release or a deployment names a repository you need the details of, or to follow a repository id from a listing. A repository of a project the token may not see, or any repository read through a guest token, is unknown rather than forbidden. - Requires ability: `repository:view` - Runs the same action as [`GET /v1/repositories/{repository}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/get-a-repository) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `repository_id` | string | yes | UUID of the repository to fetch. | ## Example call ```json { "name": "get_repository", "arguments": { "repository_id": "79fb9e03-114c-4c00-a183-318468094cca" } } ``` ## What it returns ```json { "data": { "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "provider": "github", "owner": "zuse-technologies", "name": "customer-portal-web", "full_name": "zuse-technologies/customer-portal-web", "default_branch": "main", "url": "https://github.com/zuse-technologies/customer-portal-web", "private": true, "connected_by": "9b2e4f1a-3c5d-4e6f-8a7b-1c2d3e4f5a6b", "connected_at": "2026-10-20T09:20:00+00:00", "disconnected_at": null, "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown repository_id, a repository of a project outside the token's scope or the person's visibility, or any repository read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks repository:view. ## Example prompts > "Which branch do I start from in repository `b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e`?" > "Is the customer portal repository private?" > "Who registered the payments API repository?" ## Related - [list_repositories](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/list-repositories) - [get_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-component) — the component a repository builds. - [Repositories API](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories) --- # Code Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code Where a project's code lives: the Git repositories the team registered, each with its provider, the owner and name as the provider spells them, the branch releases are cut from, the address, whether it is private and the component it builds. These tools read the registry as the token sees it, never for a guest token, so an agent clones, branches and names repositories the way the team does; and register, change and remove them (`create_repository`, `update_repository`, `delete_repository`); connecting the provider stays in the application, and the Git integration will add the branches, commits and pull requests that link work items to code. ## Tools - [`create_repository`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/create-repository) — Create Repository (destructive) - [`delete_repository`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/delete-repository) — Delete Repository (destructive) - [`get_repository`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/get-repository) — Get Repository (read) - [`list_repositories`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/list-repositories) — List Repositories (read) - [`update_repository`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/update-repository) — Update Repository (destructive) --- # list_repositories Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/list-repositories Paginated list of the Git repositories a project's code lives in, each with its provider, owner, name, default branch, privacy and component, narrowed by provider, component, disconnected state or search words. Return the repositories of one project by full name: where the team registered its code, each with the provider, the owner and name as the provider spells them, the branch releases are cut from, the address, whether it is private, the component it builds and whether it is set aside. Read it before cloning, branching or naming a repository in a commit message, so you work in the repository the team registered and branch from its default branch. Narrow with `provider` (`github`, `gitlab` or `bitbucket`), `component_id` (the repositories that build one component), `disconnected` (`true` for disconnected repositories only, `false` for live ones; omit for both) and `search` (words in the full name, the address or the default branch). A guest token is refused: where the code lives is the team's business. - Requires ability: `repository:view-any` - Runs the same action as [`GET /v1/projects/{project}/repositories`](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/list-repositories) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose repositories to list. | | `provider` | string | no | One provider: github, gitlab or bitbucket. | | `component_id` | string | no | UUID of a component; lists the repositories that build it only. | | `disconnected` | boolean | no | `true` lists disconnected repositories only, `false` live ones only; omit for both. | | `search` | string | no | Words the full name, the address or the default branch must contain. | | `limit` | integer | no | Repositories per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_repositories", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "provider": "github", "owner": "zuse-technologies", "name": "customer-portal-web", "full_name": "zuse-technologies/customer-portal-web", "default_branch": "main", "url": "https://github.com/zuse-technologies/customer-portal-web", "private": true, "connected_by": "9b2e4f1a-3c5d-4e6f-8a7b-1c2d3e4f5a6b", "connected_at": "2026-10-20T09:20:00+00:00", "disconnected_at": null, "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — provider is not one of the three providers, or component_id is not a UUID. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the person behind the token may not read the project's repositories, a client guest among them. - `TOKEN_MISSING_ABILITY` — token lacks repository:view-any. ## Example prompts > "Where does the Customer Portal's code live?" > "List the live repositories of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` and their default branches." > "Which repository builds the web app?" ## Related - [get_repository](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/get-repository) - [list_components](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) — the components a repository may build. - [Repositories API](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories) --- # update_repository Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/update-repository Changes a registered repository in part. Change a repository by `repository_id` with any of `provider`, `owner`, `name`, `url`, `default_branch`, `private` and `component_id`. A field left out keeps its value; a field sent as null is cleared. The change goes through the same action the Repositories page runs and `repository.updated` goes out with what moved. A staff token with `repository:update` on the project is needed. - Requires ability: `repository:update` - Runs the same action as [`PATCH /v1/repositories/{repository}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories/update-a-repository) - Fires events: [`repository.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `repository_id` | string | yes | UUID of the repository. | | `owner` | string | no | The account or group that owns the repository. | | `name` | string | no | The repository's name. | | `provider` | `github`, `gitlab`, `bitbucket` | no | Where the repository is hosted; github by default. | | `url` | string | no | Where it lives when not on the provider's own host. | | `default_branch` | string | no | The branch releases are cut from; main by default. | | `private` | boolean | no | Whether the repository is private; true by default. | | `component_id` | string | no | UUID of the component it builds; omit when it serves the whole project. | ## Example call ```json { "name": "update_repository", "arguments": { "repository_id": "79fb9e03-114c-4c00-a183-318468094cca" } } ``` ## What it returns ```json { "data": { "id": "4c5d6e7f-8a9b-4c0d-8e1f-2a3b4c5d6e7f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "provider": "github", "owner": "zuse-technologies", "name": "laundry-web", "full_name": "zuse-technologies/laundry-web", "default_branch": "develop", "url": "https://github.com/zuse-technologies/laundry-web", "private": true, "connected_by": null, "connected_at": null, "disconnected_at": null, "created_at": "2026-10-28T09:45:00+00:00", "updated_at": "2026-10-29T09:00:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a provider outside the three, a url that is not an address, a component_id that is not the project's, or a repository the project already registered. - `RESOURCE_NOT_FOUND` — unknown repository_id, or a repository outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry repository:update. - `TOKEN_MISSING_ABILITY` — token lacks repository:update. ## Example prompts > "Releases of the web app are cut from develop now; change the default branch." > "The API repository moved to the zuse-technologies organisation." > "Point the driver app repository at the mobile component." ## Related - [get_repository](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/get-repository) — the repository as it stands. - [delete_repository](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/code/delete-repository) — remove it from the registry. - [Repositories API](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories) — the same door over REST. --- # create_data_entity Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/create-data-entity Adds a thing the system keeps records of, with its fields, so the data dictionary a brief implies is laid down as the conversation goes. Add a data entity to a project's dictionary: `project_id` and `name` (singular, unique within the project) are required; `plural`, `description` (HTML), `component_id`, `owner_id`, `pii` (whether it holds personal data), `retention_note` and `status` (`draft` by default) fill in the record; `fields` lays down its columns in one go, each row with `name` (required), `type` (`string` by default, or `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `enum`, `uuid`, `json`, `file`, `reference`), `reference_entity_id` for a reference, `required`, `unique`, `pii`, `validation`, `example`, `description` and `enum_values` for an enum. The entity is written through the same actions the Dictionary page runs: `data_entity.created` goes out and, with fields, `data_entity.fields_changed`. Mark personal data as such on the entity and on each field: the guest view, the research sweep and the council read the flag. A staff token with `data-entity:create` on the project is needed. - Requires ability: `data-entity:create` - Runs the same action as [`POST /v1/projects/{project}/data-entities`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/create-a-data-entity) - Runs the same action as [`PUT /v1/data-entities/{entity}/fields`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/replace-a-data-entitys-fields) - Fires events: [`data_entity.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-created), [`data_entity.fields_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-fields-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `name` | string | yes | What the entity is called, in the singular, 2 to 80 characters. | | `plural` | string | no | The plural, when it is not the name with an s. | | `description` | string | no | What the entity is and what it is for, as HTML. | | `component_id` | string | no | UUID of the component whose records these are. | | `owner_id` | string | no | UUID of the team member who answers for it. | | `pii` | boolean | no | Whether it holds personal data. | | `retention_note` | string | no | How long records are kept and why. | | `fields` | array of any | no | The fields, replacing the current list: objects with name (required), type (string, text, integer, decimal, boolean, date, datetime, enum, uuid, json, file, reference), reference_entity_id, required, unique, pii, validation, example, description and enum_values; a row with an id keeps that field. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where the entity stands in review; draft by default. | ## Example call ```json { "name": "create_data_entity", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "8c9d0e1f-2a3b-4c4d-8e5f-6a7b8c9d0e1f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": null, "name": "Order", "plural": "Orders", "description": "<p>One customer's laundry order, from placement to collection.</p>", "owner_id": null, "pii": true, "retention_note": "Kept for seven years for the accounts, then anonymised.", "status": "draft", "position": 1, "fields_count": 2, "fields": [ { "id": "9d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a", "name": "reference", "type": "string", "reference_entity_id": null, "required": true, "unique": true, "pii": false, "validation": "SCL-ORD-000001", "example": "SCL-ORD-000418", "description": "The number the customer quotes.", "enum_values": [], "position": 1 }, { "id": "0e1f2a3b-4c5d-4e6f-8a7b-8c9d0e1f2a3b", "name": "status", "type": "enum", "reference_entity_id": null, "required": true, "unique": false, "pii": false, "validation": null, "example": "washing", "description": "Where the order stands.", "enum_values": ["received", "washing", "ready", "delivered"], "position": 2 } ], "created_at": "2026-10-28T09:35:00+00:00", "updated_at": "2026-10-28T09:35:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — name missing or outside 2 to 80 characters, a name another entity of the project already uses, a status outside the four, more fields than allowed, a field without a name, a type outside the list, or a reference without an entity. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry data-entity:create. - `TOKEN_MISSING_ABILITY` — token lacks data-entity:create. ## Example prompts > "Draft the data entities for the laundry shop: customer, order, order item, service, pickup slot and payment, with their fields." > "Add an Order entity with a reference, a status enum and a total; it holds personal data." > "Create a Payment entity that references Order." ## Related - [list_data_entities](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-data-entities) — the entities defined today. - [update_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-data-entity) — change the record, the fields or the status. - [Data entities API](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities) — the same doors over REST. --- # create_glossary_term Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/create-glossary-term Adds a word the project agrees on, so the vocabulary a brief uses is pinned as the conversation goes. Add a term to a project's glossary: `project_id` and `term` are required, the term unique within the project regardless of case; `definition` (HTML), `aliases` (the other names it goes by), `related_term_ids` (terms of the same project it leans on), `source` (where the definition comes from) and `status` (`draft` by default, or `in_review`, `approved`, `deprecated`) fill in the rest. The term is written through the same action the Dictionary page and the intake wizard run, and `glossary_term.created` goes out. Check `list_glossary_terms` first so a word is defined once; the editors across the project link the term wherever it appears. A staff token with `glossary:create` on the project is needed. - Requires ability: `glossary:create` - Runs the same action as [`POST /v1/projects/{project}/glossary`](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/add-a-glossary-term) - Fires events: [`glossary_term.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `term` | string | yes | The word, as the project uses it, 1 to 80 characters. | | `definition` | string | no | What it means, as HTML. | | `aliases` | array of string | no | The other names it goes by. | | `related_term_ids` | array of string | no | UUIDs of the terms of the same project it leans on. | | `source` | string | no | Where the definition comes from: a document, a person, a standard. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where the term stands in review; draft by default. | ## Example call ```json { "name": "create_glossary_term", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "term": "<term>" } } ``` ## What it returns ```json { "data": { "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "term": "Drop-off", "definition": "<p>A customer leaving laundry at the shop counter rather than booking a pickup.</p>", "aliases": ["Walk-in"], "related_term_ids": [], "source": "The shop's own words", "status": "draft", "position": 1, "created_at": "2026-10-28T09:30:00+00:00", "updated_at": "2026-10-28T09:30:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — term missing or outside 1 to 80 characters, a term the project already defines, a status outside the four, or a related term that is not the project's. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry glossary:create. - `TOKEN_MISSING_ABILITY` — token lacks glossary:create. ## Example prompts > "Define drop-off, pickup slot and wash cycle in the glossary." > "Add 'turnaround' to the glossary: the time from drop-off to ready, promised per service." > "The client calls a repeat customer a member; put that in the glossary." ## Related - [list_glossary_terms](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-glossary-terms) — the words defined today. - [update_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-glossary-term) — change or move a term. - [Glossary API](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary) — the same door over REST. --- # delete_data_entity Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-data-entity Deletes a data entity and its fields after the person confirms; deprecating keeps it on the record instead. Delete a data entity by `data_entity_id`, with its fields. Reference fields of other entities that pointed at it lose their target. The removal goes through the same action the Dictionary page runs and `data_entity.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer moving a retired entity to `deprecated` with `update_data_entity` so the record keeps it. A staff token with `data-entity:delete` on the project is needed. - Requires ability: `data-entity:delete` - Runs the same action as [`DELETE /v1/data-entities/{entity}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/delete-a-data-entity) - Fires events: [`data_entity.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `data_entity_id` | string | yes | UUID of the data entity to delete. | ## Example call ```json { "name": "delete_data_entity", "arguments": { "data_entity_id": "2cdcbc8b-a599-4f00-a4d1-43acedfca4bd" } } ``` ## What it returns ```json { "data": { "data_entity_id": "8c9d0e1f-2a3b-4c4d-8e5f-6a7b8c9d0e1f", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown data_entity_id, or an entity outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry data-entity:delete. - `TOKEN_MISSING_ABILITY` — token lacks data-entity:delete. ## Example prompts > "Delete the Basket entity; orders hold their items directly." > "Remove the duplicate Customer entity." ## Related - [update_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-data-entity) — deprecate it instead. - [list_data_entities](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-data-entities) — what is left. - [Data entities API](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities) — the same door over REST. --- # delete_glossary_term Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-glossary-term Deletes a glossary term after the person confirms; deprecating keeps the word on the record instead. Delete a glossary term by `glossary_term_id`. The removal goes through the same action the Dictionary page runs and `glossary_term.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer moving a retired word to `deprecated` with `update_glossary_term`, so the record keeps what the project used to call things. A staff token with `glossary:delete` on the project is needed. - Requires ability: `glossary:delete` - Runs the same action as [`DELETE /v1/glossary/{term}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/delete-a-glossary-term) - Fires events: [`glossary_term.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `glossary_term_id` | string | yes | UUID of the glossary term to delete. | ## Example call ```json { "name": "delete_glossary_term", "arguments": { "glossary_term_id": "ae39c24c-6919-4400-a00e-bbf51453a654" } } ``` ## What it returns ```json { "data": { "glossary_term_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown glossary_term_id, or a term outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry glossary:delete. - `TOKEN_MISSING_ABILITY` — token lacks glossary:delete. ## Example prompts > "Delete the glossary entry I added by mistake." > "Remove 'basket' from the glossary; it duplicates 'order'." ## Related - [update_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-glossary-term) — deprecate it instead. - [list_glossary_terms](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-glossary-terms) — what is left. - [Glossary API](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary) — the same door over REST. --- # get_data_entity Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-data-entity One data entity by id with its fields in order, each with its type, flags, validation, example and description. Return one entity of a project's dictionary with its fields in order: the entity's own record (name, plural, description, component, owner, personal-data flag, retention note, status) and, for every field, its name, type, the entity a reference points at, the values of an enum, whether it is required, unique or personal data, the rule a value obeys in words, a sample value and what it means. Read it before writing a migration, a form, a validation rule or an API payload for the entity, and before drafting test data, so the fields you use are the ones the analyst wrote down. An entity of a project the token may not see, or one that is not approved read through a guest token, is unknown rather than forbidden. - Requires ability: `data-entity:view` - Runs the same action as [`GET /v1/data-entities/{entity}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/get-a-data-entity) - Runs the same action as [`GET /v1/data-entities/{entity}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-a-entitys-versions) - Runs the same action as [`GET /v1/data-entities/{entity}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/read-one-version-of-a-entity) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `data_entity_id` | string | yes | UUID of the data entity to fetch. | ## Example call ```json { "name": "get_data_entity", "arguments": { "data_entity_id": "2cdcbc8b-a599-4f00-a4d1-43acedfca4bd" } } ``` ## What it returns ```json { "data": { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "name": "Order", "plural": "Orders", "description": "<p>One purchase a branch places, from the basket to the delivery note.</p>", "owner_id": "9b2e4f1a-3c5d-4e6f-8a7b-1c2d3e4f5a6b", "pii": true, "retention_note": "Kept seven years for tax; personal fields masked after two.", "status": "approved", "position": 1, "fields": [ { "id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b", "name": "reference", "type": "string", "reference_entity_id": null, "required": true, "unique": true, "pii": false, "validation": "ORD- followed by six digits", "example": "ORD-000142", "description": "The number printed on the invoice.", "enum_values": [], "position": 0 }, { "id": "f2e3d4c5-b6a7-4089-9b0c-1d2e3f4a5b6c", "name": "status", "type": "enum", "reference_entity_id": null, "required": true, "unique": false, "pii": false, "validation": null, "example": "placed", "description": "Where the order stands.", "enum_values": ["draft", "placed", "paid", "shipped"], "position": 1 } ], "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown data_entity_id, an entity of a project outside the token's scope or the person's visibility, or an entity that is not approved read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks data-entity:view. ## Example prompts > "What fields does an Order carry, and which are required?" > "Read entity `a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d` and draft the migration for it." > "Which fields of the Customer entity are personal data?" ## Related - [list_data_entities](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-data-entities) - [get_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-glossary-term) — the project's meaning of a word a field or a value uses. - [Data Entities API](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities) --- # get_glossary_term Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-glossary-term One glossary term by id, with its definition, other names, related terms, source and review status. Return one term of a project's glossary: the word, its definition as HTML, the other names it goes by, the ids of the terms it leans on, where the definition comes from and where it stands in its review. Read it when a story, a requirement or a brief uses a word you need the project's exact meaning of, or to follow a `related_term_ids` entry from another term. A term of a project the token may not see, or a term that is not approved read through a guest token, is unknown rather than forbidden. - Requires ability: `glossary:view` - Runs the same action as [`GET /v1/glossary/{term}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/get-a-glossary-term) - Runs the same action as [`GET /v1/glossary/{term}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-a-terms-versions) - Runs the same action as [`GET /v1/glossary/{term}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/read-one-version-of-a-term) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `glossary_term_id` | string | yes | UUID of the glossary term to fetch. | ## Example call ```json { "name": "get_glossary_term", "arguments": { "glossary_term_id": "ae39c24c-6919-4400-a00e-bbf51453a654" } } ``` ## What it returns ```json { "data": { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "term": "Branch", "definition": "<p>One physical shop of the client, with its own stock, staff and opening hours.</p>", "aliases": ["Site", "Location"], "related_term_ids": ["e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81"], "source": "Client operations manual, 2025 edition", "status": "approved", "position": 0, "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown glossary_term_id, a term of a project outside the token's scope or the person's visibility, or a term that is not approved read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks glossary:view. ## Example prompts > "What does the project mean by a branch?" > "Read glossary term `d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70` and the terms it relates to." > "Is the definition of Basket approved yet?" ## Related - [list_glossary_terms](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-glossary-terms) - [get_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-data-entity) — a thing the project keeps records of, field by field. - [Glossary API](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary) --- # Dictionary Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary The dictionary of a project: the glossary (the words the project agrees on, each with one definition, the other names it goes by, the terms it leans on, its source and its review status) and the data entities (the things the project keeps records of, each with its fields, their types, flags, validation and examples, its component, its owner, whether it holds personal data and how long it is kept). These tools read both as the token sees them, a guest's token reading the approved entries only, so an agent drafts in the project's own words and designs a table, a form or a payload against the fields the analyst wrote down, and, for a staff token, writes them: a term (`create_glossary_term`, `update_glossary_term`, which also moves it along its review) and an entity with its fields in one call (`create_data_entity`, `update_data_entity`), each with a delete that pauses for confirmation. ## Tools - [`create_data_entity`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/create-data-entity) — Create Data Entity (destructive) - [`create_glossary_term`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/create-glossary-term) — Create Glossary Term (destructive) - [`delete_data_entity`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-data-entity) — Delete Data Entity (destructive) - [`delete_glossary_term`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-glossary-term) — Delete Glossary Term (destructive) - [`get_data_entity`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-data-entity) — Get Data Entity (read) - [`get_glossary_term`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-glossary-term) — Get Glossary Term (read) - [`list_data_entities`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-data-entities) — List Data Entities (read) - [`list_glossary_terms`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-glossary-terms) — List Glossary Terms (read) - [`update_data_entity`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-data-entity) — Update Data Entity (destructive) - [`update_glossary_term`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-glossary-term) — Update Glossary Term (destructive) --- # list_data_entities Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-data-entities Paginated list of the things a project keeps records of, each with its component, owner, personal-data flag, retention note, review status and field count, narrowed by status, component, personal data or search words. Return the data entities of one project in the project's order: each thing the analyst wrote down as a record the system keeps, with its plural, its description as HTML, the component whose records these are, the member who answers for it, whether it holds personal data, how long it is kept, where it stands in its review and how many fields it carries. Read it before designing a table, a form or an API payload, to learn what the business keeps records of in its own words; then read one entity with `get_data_entity` for its fields. Narrow with `status` (`draft`, `in_review`, `approved` or `deprecated`), `component_id` (one component's entities), `pii` (`true` for entities holding personal data only) and `search` (words in the name, the plural or the description). A guest token reads the approved entities only, whatever `status` asks for. - Requires ability: `data-entity:view-any` - Runs the same action as [`GET /v1/projects/{project}/data-entities`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/list-data-entities) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose data entities to list. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Narrow to one status. | | `component_id` | string | no | Narrow to the entities of one component, by UUID. | | `pii` | boolean | no | `true` lists the entities that hold personal data only. | | `search` | string | no | Words the name, the plural or the description must contain. | | `limit` | integer | no | Entities per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_data_entities", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "name": "Order", "plural": "Orders", "description": "<p>One purchase a branch places, from the basket to the delivery note.</p>", "owner_id": "9b2e4f1a-3c5d-4e6f-8a7b-1c2d3e4f5a6b", "pii": true, "retention_note": "Kept seven years for tax; personal fields masked after two.", "status": "approved", "position": 1, "fields_count": 2, "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — status is not one of the four statuses, component_id is not a UUID, or search is longer than 120 characters. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks data-entity:view-any. ## Example prompts > "What does the Customer Portal keep records of?" > "List the entities of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` that hold personal data." > "Which entities belong to the API component?" ## Related - [get_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-data-entity) — one entity with its fields. - [list_glossary_terms](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-glossary-terms) — the words the project uses, as opposed to the records it keeps. - [list_components](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) — the components an entity's records belong to. - [Data Entities API](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities) --- # list_glossary_terms Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-glossary-terms Paginated list of the words a project agrees on, each with its definition, other names, related terms, source and review status, narrowed by status or search words. Return the glossary of one project in the project's order: the words the analyst wrote down with one definition each, the other names they go by, the terms they lean on, where the definition comes from and where each stands in its review. Read it before drafting a story, a requirement, a rule or a test, so you write in the project's own words rather than your own, and read the definition of any word the brief uses in a way you did not expect. Narrow with `status` (`draft`, `in_review`, `approved` or `deprecated`) and `search` (words in the term, an alias or the definition). A guest token reads the approved terms only, whatever `status` asks for. - Requires ability: `glossary:view-any` - Runs the same action as [`GET /v1/projects/{project}/glossary`](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/list-glossary-terms) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose glossary to list. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Narrow to one status. | | `search` | string | no | Words the term, an alias or the definition must contain. | | `limit` | integer | no | Terms per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_glossary_terms", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "term": "Branch", "definition": "<p>One physical shop of the client, with its own stock, staff and opening hours.</p>", "aliases": ["Site", "Location"], "related_term_ids": ["e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81"], "source": "Client operations manual, 2025 edition", "status": "approved", "position": 0, "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — status is not one of the four statuses, or search is longer than 120 characters. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks glossary:view-any. ## Example prompts > "What words does the Customer Portal project define?" > "List the approved glossary terms of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`." > "Does the project mean the same thing by branch and site?" ## Related - [get_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-glossary-term) - [list_data_entities](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/list-data-entities) — the things the project keeps records of, as opposed to the words it uses. - [Glossary API](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary) --- # update_data_entity Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-data-entity Changes a data entity in part, replaces its fields when a list is sent and moves it along its review when a status is sent. Change a data entity by `data_entity_id` with any of `name`, `plural`, `description` (HTML), `component_id`, `owner_id`, `pii`, `retention_note`, `fields` and `status` (`draft`, `in_review`, `approved` or `deprecated`; a move the lifecycle allows). A field left out keeps its value; a field sent as null is cleared. `fields` replaces the list: a row with an `id` keeps and changes that field, a row without one is new, and a field left out of the list goes. The record goes through the update action, the fields through the sync and the status through its own move, so `data_entity.updated`, `data_entity.fields_changed` and `data_entity.status_changed` go out as each applies. A staff token with `data-entity:update` on the project is needed. - Requires ability: `data-entity:update` - Runs the same action as [`PATCH /v1/data-entities/{entity}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/update-a-data-entity) - Runs the same action as [`POST /v1/data-entities/{entity}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/move-a-data-entity) - Runs the same action as [`PUT /v1/data-entities/{entity}/fields`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/replace-a-data-entitys-fields) - Fires events: [`data_entity.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-updated), [`data_entity.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-status-changed), [`data_entity.fields_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-fields-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `data_entity_id` | string | yes | UUID of the data entity. | | `name` | string | no | What the entity is called, in the singular, 2 to 80 characters. | | `plural` | string | no | The plural, when it is not the name with an s. | | `description` | string | no | What the entity is and what it is for, as HTML. | | `component_id` | string | no | UUID of the component whose records these are. | | `owner_id` | string | no | UUID of the team member who answers for it. | | `pii` | boolean | no | Whether it holds personal data. | | `retention_note` | string | no | How long records are kept and why. | | `fields` | array of any | no | The fields, replacing the current list: objects with name (required), type (string, text, integer, decimal, boolean, date, datetime, enum, uuid, json, file, reference), reference_entity_id, required, unique, pii, validation, example, description and enum_values; a row with an id keeps that field. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where the entity stands in review; a move the lifecycle allows. | ## Example call ```json { "name": "update_data_entity", "arguments": { "data_entity_id": "2cdcbc8b-a599-4f00-a4d1-43acedfca4bd" } } ``` ## What it returns ```json { "data": { "id": "8c9d0e1f-2a3b-4c4d-8e5f-6a7b8c9d0e1f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "name": "Order", "plural": "Orders", "description": "<p>One customer's laundry order, from placement to collection.</p>", "owner_id": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "pii": true, "retention_note": "Kept for seven years for the accounts, then anonymised.", "status": "in_review", "position": 1, "fields_count": 3, "fields": [ { "id": "9d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a", "name": "reference", "type": "string", "reference_entity_id": null, "required": true, "unique": true, "pii": false, "validation": null, "example": "SCL-ORD-000418", "description": null, "enum_values": [], "position": 1 }, { "id": "0e1f2a3b-4c5d-4e6f-8a7b-8c9d0e1f2a3b", "name": "status", "type": "enum", "reference_entity_id": null, "required": true, "unique": false, "pii": false, "validation": null, "example": null, "description": null, "enum_values": ["received", "washing", "ready", "delivered"], "position": 2 }, { "id": "1f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c", "name": "customer", "type": "reference", "reference_entity_id": "2a3b4c5d-6e7f-4a8b-8c9d-0e1f2a3b4c5d", "required": true, "unique": false, "pii": false, "validation": null, "example": null, "description": "The customer who placed it.", "enum_values": [], "position": 3 } ], "created_at": "2026-10-28T09:35:00+00:00", "updated_at": "2026-10-28T16:40:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a name another entity of the project already uses, a row without a name, a type outside the list, a reference without an entity, or a move the lifecycle does not allow from where the entity stands. - `RESOURCE_NOT_FOUND` — unknown data_entity_id, or an entity outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry data-entity:update. - `TOKEN_MISSING_ABILITY` — token lacks data-entity:update. ## Example prompts > "Add a customer reference field to the Order entity and send it for review." > "Mark the Customer entity as personal data with a two-year retention." > "Replace the Payment entity's fields with amount, currency, provider and paid_at." ## Related - [get_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-data-entity) — the entity with its fields as they stand. - [delete_data_entity](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-data-entity) — take it away. - [Data entities API](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities) — the same doors over REST. --- # update_glossary_term Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/update-glossary-term Changes a glossary term in part and moves it along its review when a status is sent. Change a glossary term by `glossary_term_id` with any of `term`, `definition` (HTML), `aliases` and `related_term_ids` (each replaces its list), `source` and `status` (`draft`, `in_review`, `approved` or `deprecated`; a move the lifecycle allows from where the term stands). A field left out keeps its value; a field sent as null is cleared. The fields go through the same action the Dictionary page runs and the status through its own move, so `glossary_term.updated` and, on a move, `glossary_term.status_changed` go out. A staff token with `glossary:update` on the project is needed. - Requires ability: `glossary:update` - Runs the same action as [`PATCH /v1/glossary/{term}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/update-a-glossary-term) - Runs the same action as [`POST /v1/glossary/{term}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/move-a-glossary-term) - Fires events: [`glossary_term.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-updated), [`glossary_term.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-status-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `glossary_term_id` | string | yes | UUID of the glossary term. | | `term` | string | no | The word, as the project uses it, 1 to 80 characters. | | `definition` | string | no | What it means, as HTML. | | `aliases` | array of string | no | The other names it goes by. | | `related_term_ids` | array of string | no | UUIDs of the terms of the same project it leans on. | | `source` | string | no | Where the definition comes from: a document, a person, a standard. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Where the term stands in review; a move the lifecycle allows. | ## Example call ```json { "name": "update_glossary_term", "arguments": { "glossary_term_id": "ae39c24c-6919-4400-a00e-bbf51453a654" } } ``` ## What it returns ```json { "data": { "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "term": "Drop-off", "definition": "<p>A customer leaving laundry at the shop counter rather than booking a pickup.</p>", "aliases": ["Walk-in", "Counter order"], "related_term_ids": ["8b9c0d1e-2f3a-4b4c-8d5e-6f7a8b9c0d1e"], "source": "The shop's own words", "status": "approved", "position": 1, "created_at": "2026-10-28T09:30:00+00:00", "updated_at": "2026-10-28T15:12:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a term another entry of the project already uses, a related term that is not the project's, a status outside the four, or a move the lifecycle does not allow from where the term stands. - `RESOURCE_NOT_FOUND` — unknown glossary_term_id, or a term outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry glossary:update. - `TOKEN_MISSING_ABILITY` — token lacks glossary:update. ## Example prompts > "Approve the drop-off term and add 'counter order' as an alias." > "Link turnaround to wash cycle in the glossary." > "Deprecate the term 'ticket'; the client says 'order' now." ## Related - [get_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/get-glossary-term) — the term as it stands. - [delete_glossary_term](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/dictionary/delete-glossary-term) — take it away. - [Glossary API](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary) — the same door over REST. --- # generate_document Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/generate-document Asks a project for a business requirements document or a software requirements specification as the token's holder, with a title, a file format and a scope, and answers with the document while it renders in the background. Ask for a document the way the Generate menu does. `project_id` and `kind` (`brd` or `srs`; the other kinds are refused until their modules land) are required; `title` is the kind's name by default (3 to 160 characters), `format` is `pdf` by default or `html`, `baseline_id` names a frozen baseline of the project so the document prints exactly what the client signed (omit it for the record as it stands), `drafts` true prints records still in draft beside the approved ones, and `summary` is the opening page as plain text up to 6,000 characters; omit it and Heimdall drafts the page when the render runs, which spends the team's AI budget, so send the words when the person already has them. The document is written as `rendering`, numbered within its kind, and announced with `document.created`; the render runs on the queue as the token's holder, reading the record under their own permissions, and ends in `document.rendered` or `document.failed`. Read it back with `get_document` once its status is `draft`; a person reviews the draft, edits the opening page, and puts it to the project manager, which an agent never does for them. Runs the same action as **Generate** in the application. - Requires ability: `document:create` - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `kind` | `brd`, `srs` | yes | What to generate: brd or srs. | | `title` | string | no | The title on the cover, 3 to 160 characters; the kind's name by default. | | `format` | `pdf`, `html` | no | The file to render: pdf by default, or html. | | `baseline_id` | string | no | UUID of a frozen baseline to read from; omit for the record as it stands. | | `drafts` | boolean | no | True to print records still in draft beside the approved ones. | | `summary` | string | no | The opening prose, plain text up to 6,000 characters; omit it and Heimdall drafts it. | ## Example call ```json { "name": "generate_document", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "kind": "brd" } } ``` ## What it returns ```json { "data": { "id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "kind": "brd", "reference": "BRD #3", "number": 3, "title": "BRD for the client workshop", "format": "pdf", "status": "rendering", "editable": false, "readable": false, "scope": { "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "drafts": false }, "summary": "", "size": null, "checksum": null, "version": 0, "approved_at": null, "approved_revision_id": null, "rendered_by": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "renderer_name": "Bea Analyst", "rendered_at": null, "finalised_at": null, "superseded_by_id": null, "render_failure": null, "download_url": null, "created_at": "2026-11-05T09:00:00+00:00", "updated_at": "2026-11-05T09:00:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a kind or a format that is not open yet, a title outside 3 to 160 characters, a summary over 6,000 characters, a baseline_id that is not a UUID, or a baseline of the project that is still a draft. - `RESOURCE_NOT_FOUND` — unknown project_id, a project outside the token's scope, or a baseline_id that is unknown or another project's; nothing is written. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry document:create. - `TOKEN_MISSING_ABILITY` — token lacks document:create. ## Example prompts > "Generate the BRD for the Customer Portal from the signed v1.0 scope, as a PDF." > "Make an HTML SRS of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` with the drafts included; I will read it myself." > "Generate a BRD titled 'BRD for the client workshop' and use this executive summary: …" ## Related - [get_document](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/get-document) — read the draft once it rendered, with its words. - [list_documents](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/list-documents) — the project's documents and which copy is final. - [list_baselines](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-baselines) — find the frozen baseline to render from. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — put the rendered draft to the project manager, on the person's word. - [Documents API](https://alm-docs.labs.zuselab.dev/api/v1/reference/documents) — the same write over REST. --- # get_document Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/get-document Fetches one generated document by id with its kind, reference, title, format, status, scope, opening prose, stamps and download address, and on request the rendered document as plain words. Read one document in full. Beside the row `list_documents` gives (kind, reference, title, format, status, scope, the opening prose as `summary`, size and checksum, version, the approval stamps, who rendered it and when, when it became final, `superseded_by_id`, `render_failure` and `download_url`), pass `with_text` true to receive `text`: the rendered document as plain words, up to 60,000 characters, so you read a BRD or an SRS without downloading it. An HTML document answers with its markup stripped and its lines kept; a PDF answers with its opening prose only, since the file is laid out for paper, so ask the team for an HTML copy when you need the whole SRS. A client guest's token reads final and superseded copies only; a draft it asks for is unknown rather than forbidden. - Requires ability: `document:view` - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `document_id` | string | yes | UUID of the document to fetch. | | `with_text` | boolean | no | True to include the rendered document as plain words. | ## Example call ```json { "name": "get_document", "arguments": { "document_id": "a0cd9724-0b51-4400-aefa-ee9d6f73b0bc" } } ``` ## What it returns ```json { "data": { "id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "kind": "srs", "reference": "SRS #1", "number": 1, "title": "Software Requirements Specification", "format": "html", "status": "draft", "editable": true, "readable": true, "scope": { "baseline_id": null, "drafts": false }, "summary": "The team builds the Customer Portal against this specification.", "size": 183204, "checksum": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", "version": 0, "approved_at": null, "approved_revision_id": null, "rendered_by": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "renderer_name": "Bea Analyst", "rendered_at": "2026-11-03T08:05:00+00:00", "finalised_at": null, "superseded_by_id": null, "render_failure": null, "download_url": "https://api.example.test/v1/documents/d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70/download", "created_at": "2026-11-03T08:00:00+00:00", "updated_at": "2026-11-03T08:05:00+00:00", "text": "Software Requirements Specification\nCustomer Portal (CRM)\n\n1. Introduction\nThe team builds the Customer Portal against this specification.\n\n3. Functional requirements\nCRM-FR-12 Password reset by email …" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown document_id, a document of a project outside the token's scope, or, for a client guest's token, a document that is not final or superseded. - `FORBIDDEN` — the holder's role on the project does not carry document:view. - `TOKEN_MISSING_ABILITY` — token lacks document:view. ## Example prompts > "Read SRS #1 of the Customer Portal and list every functional requirement about passwords." > "What does the executive summary of the final BRD say the sponsor is asked to do?" > "Open document `d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70` with its text and tell me which use cases it covers." ## Related - [list_documents](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/list-documents) — the project's documents with their statuses. - [generate_document](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/generate-document) — ask for a fresh copy when the record moved on. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — the same record, story by story, for the work in hand. - [Documents API](https://alm-docs.labs.zuselab.dev/api/v1/reference/documents) — the same read over REST, and the file's bytes. --- # Documents Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents The documents a project writes from its own record: the business requirements document the client signs and the software requirements specification the team builds against, each asked for with a kind, a title, a file format (PDF or HTML) and a scope (the record as it stands, with or without drafts, or a frozen baseline), rendered in the background as the person who asked and under their permissions, numbered within its kind (`BRD #2`), with an opening page Heimdall drafts when nobody wrote it, moved to final by the project manager's approval, which supersedes the kind's earlier final copy, and shared with the client's guests from that moment. These tools read a project's documents and which copy is final (`list_documents`), read one with its scope, its opening page and, on request, the rendered words, so an agent reads a BRD or an SRS without downloading it (`get_document`), and ask for a new one as the token's holder (`generate_document`), after which a person reviews the draft, edits the page and puts it to the project manager through `submit_for_review`; editing a draft, rendering it again, superseding a final copy and deleting a document stay in the application and the REST API. ## Tools - [`generate_document`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/generate-document) — Generate Document (destructive) - [`get_document`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/get-document) — Get Document (read) - [`list_documents`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/list-documents) — List Documents (read) --- # list_documents Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/list-documents Lists a project's generated documents newest first with their kind, reference, title, format, status, scope, stamps and download address, narrowed by kind, status or words; a guest token sees the final copies only. Read the documents a project generated from its own record before you answer a question about what the client was shown or what the team builds against. Each row carries the `kind` (`brd` or `srs`), the `reference` (_BRD #2_: documents are numbered within their kind on the project), the `title`, the `format` (`pdf` or `html`), the `status` (`rendering`, `draft`, `failed`, `in_review`, `final` or `superseded`), `editable` and `readable` (whether a draft can still change, and whether there is a file to fetch), the `scope` it was rendered from (`baseline_id` and `drafts`), the opening prose as `summary`, the file's `size` and `checksum`, the `version` (0 until approved, 1 once final), the approval stamps, who rendered it and when, when it became final, which copy superseded it, the reason a render failed, and `download_url`, the REST address of the file when there is one. Narrow with `kind`, `status` or `search` (words the title must contain) and page with `limit` and `page`. Rows are newest first. A client guest's token receives the final and superseded copies only; read one document in full, or its words, with `get_document`. - Requires ability: `document:view-any` - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `kind` | `brd`, `srs`, `sprint_report`, `release_notes`, `uat_sign_off_sheet`, `test_summary`, `post_mortem`, `status_report`, `traceability_matrix`, `timesheet` | no | Narrow to one kind. | | `status` | `rendering`, `draft`, `failed`, `in_review`, `final`, `superseded` | no | Narrow to one status. | | `search` | string | no | Words the title must contain. | | `limit` | integer | no | Rows per page, 1 to 100; 25 by default. | | `page` | integer | no | The page, from 1. | ## Example call ```json { "name": "list_documents", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "kind": "brd", "reference": "BRD #2", "number": 2, "title": "BRD for the client workshop", "format": "pdf", "status": "final", "editable": false, "readable": true, "scope": { "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "drafts": false }, "summary": "The client needs a portal that serves its customers around the clock.\n\nThis document covers the first release.", "size": 48213, "checksum": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "version": 1, "approved_at": "2026-11-04T10:12:00+00:00", "approved_revision_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "rendered_by": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "renderer_name": "Bea Analyst", "rendered_at": "2026-11-03T08:05:00+00:00", "finalised_at": "2026-11-04T10:12:00+00:00", "superseded_by_id": null, "render_failure": null, "download_url": "https://api.example.test/v1/documents/d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70/download", "created_at": "2026-11-03T08:00:00+00:00", "updated_at": "2026-11-04T10:12:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 3, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — unknown kind or status (the response lists the supported values), or limit outside 1..100. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope. - `FORBIDDEN` — the holder's role on the project does not carry document:view-any. - `TOKEN_MISSING_ABILITY` — token lacks document:view-any. ## Example prompts > "Which BRD is the final one on the Customer Portal, and when did the client get it?" > "List the documents of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` that are still drafts." > "Is there an SRS rendered from the signed scope yet?" ## Related - [get_document](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/get-document) — one document with, on request, the rendered words. - [generate_document](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/documents/generate-document) — ask for a BRD or an SRS as the token's holder. - [list_baselines](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-baselines) — the frozen scope a client copy is rendered from. - [Documents API](https://alm-docs.labs.zuselab.dev/api/v1/reference/documents) — the same list over REST, and the file itself. --- # create_webhook_endpoint Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/create-webhook-endpoint Register an outbound webhook endpoint for the token's team. The signing secret is returned in this response only. Register a URL for Zuse Bifrost to post events to, with the event names it should receive. The endpoint belongs to the token's team; pass `project_id` to deliver only one project's events, and `allowed_ips` to pin the consumer's addresses. > **The secret is returned once** > > The response carries `secret` alongside the endpoint row. It is the key every > delivery to this endpoint is signed with, it is never readable again, not by > [`get_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-endpoint), > not in the application, and it can only be replaced with > [`rotate_webhook_endpoint_secret`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/rotate-webhook-endpoint-secret). > Hand it to the consumer immediately. This is the one exception to the server's > rule that a secret is never surfaced. The URL must be `https` and resolve to a public address; `http` and private-network targets are refused. Deliveries start at once unless `is_active` is `false`. - Requires ability: `webhook-endpoint:create` - Runs the same action as [`POST /v1/webhook-endpoints`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/register-a-webhook-endpoint) - Annotations: Destructive, Open world ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | A label for the endpoint, 3 to 100 characters, shown on the webhooks settings page. | | `url` | string | yes | The https URL events are posted to. Must resolve to a public address; http and private-network targets are refused. | | `events` | array of string | yes | Event names to deliver, from zusegenius://enums/webhook-event. At least one. | | `project_id` | string | no | Optional project UUID: deliver only that project's events. Omit for every project of the team. | | `allowed_ips` | array of string | no | Optional list of IP addresses or CIDR ranges the endpoint host may resolve to; a delivery whose host resolves outside the list is dead-lettered without being posted. | | `is_active` | boolean | no | Whether deliveries start right away. Defaults to true. | ## Example call ```json { "name": "create_webhook_endpoint", "arguments": { "name": "<name>", "url": "https://example.com", "events": [] } } ``` ## What it returns ```json { "data": { "id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "name": "Delivery board", "url": "https://hooks.example.test/zusegenius", "project_id": null, "events": ["project.created", "project.status_changed"], "is_active": true, "disabled_at": null, "allowed_ips": [], "failure_count": 0, "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "created_at": "2026-10-26T09:20:00+00:00", "updated_at": "2026-10-26T09:20:00+00:00", "secret": "whsec_..." }, "meta": {} } ``` Every event name must be one the token could subscribe to: each event requires the ability of its family (`project.*` needs `project:view`, `team.*` needs `team:view`, and so on), which is checked at registration. The `zusegenius://enums/webhook-event` resource lists every event with the ability it needs. ## How it fails - `VALIDATION_FAILED` — per field: an http or private-network URL, an empty or unknown events list, an event the token cannot subscribe to, an allowed_ips entry that is neither an IP address nor a CIDR range. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-endpoint:create. ## Example prompts > "Register `https://hooks.example.test/zusegenius` for every project event, named 'Delivery board'." > "Create a webhook endpoint that receives only `project.status_changed` for project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`." > "Set up an endpoint for token events, paused until we have deployed the handler." ## Related - [list_webhook_endpoints](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-endpoints) — what the team already has. - [test_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/test-webhook-endpoint) — check the consumer end to end. - [rotate_webhook_endpoint_secret](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/rotate-webhook-endpoint-secret) — the only way to get a new secret. - [Webhook endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) — the REST equivalent. - [Signature verification](https://alm-docs.labs.zuselab.dev/webhooks/v1/signature-verification) — what the consumer does with the secret. --- # delete_webhook_endpoint Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/delete-webhook-endpoint Remove an outbound webhook endpoint. Destructive — an integration listening on it goes dark at once; its delivery log is kept. Remove an endpoint the team no longer wants. Deliveries to it stop immediately and its delivery log is kept for reference. If the intent is "stop it for now", [`pause_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/pause-webhook-endpoint) does that reversibly and keeps the secret. > **Destructive — confirm the endpoint with a person first** > > Whatever is listening on the URL stops receiving events with no notice. > Confirm the exact `endpoint_id` with the person before calling. Only the person who registered the endpoint, or the team owner, may remove it; a fellow team member is refused with `FORBIDDEN`. Idempotent: an already-deleted id surfaces as `RESOURCE_NOT_FOUND`, exactly as an unknown or foreign id does. - Requires ability: `webhook-endpoint:delete` - Runs the same action as [`DELETE /v1/webhook-endpoints/{endpoint}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/delete-a-webhook-endpoint) - Annotations: Destructive, Idempotent ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `endpoint_id` | string | yes | UUID of the webhook endpoint to delete. | ## Example call ```json { "name": "delete_webhook_endpoint", "arguments": { "endpoint_id": "231540dc-049d-4400-a42d-dc69ba798b74" } } ``` ## What it returns ```json { "data": { "endpoint_id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "deleted": true }, "meta": {} } ``` ## How it fails - `FORBIDDEN` — the token belongs to a team member who did not register the endpoint and is not the team owner. - `RESOURCE_NOT_FOUND` — unknown or already-deleted endpoint_id, or an endpoint of another team. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-endpoint:delete. ## Example prompts > "Delete webhook endpoint `c62a08f4-1b7d-4e35-9860-a37f5d21e0b9`; the board it fed was decommissioned." (confirm first)" > "Remove the old staging endpoint from the team." > "We're replacing the hooks URL; delete the current endpoint after the new one is registered." ## Related - [pause_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/pause-webhook-endpoint) — the reversible alternative. - [list_webhook_endpoints](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-endpoints) — what is left. - [Webhook endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) — the REST equivalent, a 204. --- # get_activity_log Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-activity-log Read the activity log of one subject, newest first — every change made to it, who made it, the surface it came through and the columns that changed. Read the history of one subject: every change made to it from the application, the REST API, an MCP tool, an accepted Heimdall draft or a scheduled job, each with who caused it, the surface it came through (`actor_kind`: `human`, `api_token`, `mcp`, `ai`, `integration`, `webhook` or `system`) and the columns that changed. Keys named like a secret are scrubbed before the response returns. The subject itself is resolved first, so an id the token cannot see answers `RESOURCE_NOT_FOUND` and learns nothing from the history. > **Note** > > Pass a short alias as `subject_type`: `project`, `client` or `project-member` > today, with every artefact alias joining as its module lands. The `subject_id` > is the row UUID. `limit` is 1 to 200, 50 when omitted. - Requires ability: `activity:read` - Runs the same action as [`GET /v1/activity`](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity/read-a-subjects-activity) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `subject_type` | `project`, `client`, `project-member`, `component`, `environment`, `stakeholder`, `persona`, `glossary-term`, `data-entity`, `data-field`, `design-reference`, `repository`, `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision` | yes | The kind of subject, as a short alias. | | `subject_id` | string | yes | UUID of the subject. | | `limit` | integer | no | How many of the newest entries to return (1..200, default 50). | ## Example call ```json { "name": "get_activity_log", "arguments": { "subject_type": "project", "subject_id": "0de9d6ed-982e-4f00-a6c2-150ac4fe372e" } } ``` ## What it returns ```json { "data": [ { "id": "92e4c1b6-70da-4f38-8517-b036ae94d7c2", "log_name": "default", "description": "Project Customer Portal was updated.", "event": "updated", "subject_type": "project", "subject_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "causer_type": "user", "causer_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "actor_kind": "human", "changes": { "status": { "from": "planning", "to": "in_development" } }, "properties": {}, "created_at": "2026-10-24T16:40:00+00:00" } ], "meta": { "subject_type": "project", "subject_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "total": 12, "limit": 50 } } ``` ## How it fails - `TOKEN_MISSING_ABILITY` — token lacks activity:read. - `VALIDATION_FAILED` — subject_type is not one of the supported aliases (model class names are refused; the error context lists the aliases), or subject_id is not a UUID. - `RESOURCE_NOT_FOUND` — the subject does not exist, belongs to another team, or sits outside the token's scope:project: allow-list. ## Example prompts > "What changed on project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` this week, and who changed it?" > "Show me the last 20 changes to the Customer Portal project made through the API or an agent." ## Related - [get_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project) - [list_projects](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-projects) - [Activity API](https://alm-docs.labs.zuselab.dev/api/v1/reference/activity) - [Activity log](https://alm-docs.labs.zuselab.dev/workspace/activity-log) --- # get_webhook_delivery Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-delivery One outbound webhook delivery by UUID — its event, status, attempts, the payload that was posted and what the endpoint answered. Read one delivery back, typically the row [`test_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/test-webhook-endpoint) returned or one picked out of [`list_webhook_deliveries`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries), to see whether the worker has posted it yet and what the target said. The row shape is the list's. - Requires ability: `webhook-delivery:view` - Runs the same action as [`GET /v1/webhook-deliveries/{delivery}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/get-a-webhook-delivery) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `delivery_id` | string | yes | UUID of the webhook delivery to fetch. | ## Example call ```json { "name": "get_webhook_delivery", "arguments": { "delivery_id": "f79655c3-045f-4800-a9a1-2c7afa3ac6e5" } } ``` ## What it returns ```json { "data": { "id": "34f1d78e-05a2-4b69-8c3d-7e921ab06f45", "endpoint_id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "event": "project.status_changed", "event_id": "01HXZ3Q8M7Y2K4N6P9R1T3V5W7", "status": "delivered", "attempts": 3, "response_status": 200, "response_excerpt": "{\"ok\":true}", "payload": { "id": "evt_01HXZ3Q8M7Y2K4N6P9R1T3V5W7", "type": "project.status_changed", "created_at": "2026-10-24T16:40:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "key": "CRM", "previous_status": "planning", "status": "in_development" } }, "next_attempt_at": null, "delivered_at": "2026-10-24T17:06:00+00:00", "dead_lettered_at": null, "created_at": "2026-10-24T16:40:00+00:00" }, "meta": {} } ``` `attempts` counts every post made so far, so a `delivered` row with `attempts: 3` succeeded on the third try. `event_id` is the envelope's `id` without its `evt_` prefix, the same value the consumer saw in the `ZG-Event-Id` header, which is how a row here is matched to a line in the consumer's own log. ## How it fails - `RESOURCE_NOT_FOUND` — unknown delivery_id, or a delivery to another team's endpoint. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-delivery:view. ## Example prompts > "Has delivery `34f1d78e-05a2-4b69-8c3d-7e921ab06f45` gone out yet, and what did the endpoint answer?" > "Show me the exact payload our board received for that event." > "Why is that delivery still pending?" ## Related - [list_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries) — find the row. - [retry_webhook_delivery](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-webhook-delivery) — replay it if it failed. - [Webhook deliveries API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries) — the REST equivalent. --- # get_webhook_endpoint Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-endpoint One of the team's outbound webhook endpoints by UUID, with its health counters. The signing secret is never returned. Read one endpoint back: its target, the events it receives, whether it is active, and the health counters the application shows, the lifetime failure total, the current consecutive-failure streak and the last success and failure instants. The row is the one [`list_webhook_endpoints`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-endpoints) returns. > **Never the secret** > > The signing secret exists in the > [`create_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/create-webhook-endpoint) > and > [`rotate_webhook_endpoint_secret`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/rotate-webhook-endpoint-secret) > responses only. If a consumer has lost it, rotate. - Requires ability: `webhook-endpoint:view` - Runs the same action as [`GET /v1/webhook-endpoints/{endpoint}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/get-a-webhook-endpoint) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `endpoint_id` | string | yes | UUID of the webhook endpoint to fetch. | ## Example call ```json { "name": "get_webhook_endpoint", "arguments": { "endpoint_id": "231540dc-049d-4400-a42d-dc69ba798b74" } } ``` ## What it returns ```json { "data": { "id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "name": "Delivery board", "url": "https://hooks.example.test/zusegenius", "project_id": null, "events": ["project.created", "project.status_changed"], "is_active": true, "disabled_at": null, "allowed_ips": [], "failure_count": 2, "consecutive_failures": 0, "last_success_at": "2026-10-25T18:00:00+00:00", "last_failure_at": "2026-10-20T07:12:00+00:00", "created_at": "2026-10-01T10:05:00+00:00", "updated_at": "2026-10-25T18:00:00+00:00" }, "meta": {} } ``` `consecutive_failures` is the streak that disables an endpoint when it runs too long; `failure_count` is the lifetime total. `disabled_at` is set when the endpoint is paused, by a person or by the streak. A null `project_id` means the endpoint receives the whole team's events. ## How it fails - `RESOURCE_NOT_FOUND` — unknown endpoint_id, or an endpoint of another team. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-endpoint:view. ## Example prompts > "Is webhook endpoint `c62a08f4-1b7d-4e35-9860-a37f5d21e0b9` healthy? When did it last succeed?" > "Which events does the Delivery board endpoint receive?" > "Is that endpoint paused, and since when?" ## Related - [list_webhook_endpoints](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-endpoints) — every endpoint of the team. - [list_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries) — the deliveries behind the counters. - [Webhook endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) — the REST equivalent. --- # Observability Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability Where to look when something did not happen: outbound webhook endpoints and their deliveries, and the activity log of every change made to a subject and who made it, from the application, the REST API, an MCP tool, an accepted Heimdall draft or a scheduled job. These tools read and replay that record. ## Tools - [`create_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/create-webhook-endpoint) — Create Webhook Endpoint (destructive) - [`delete_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/delete-webhook-endpoint) — Delete Webhook Endpoint (destructive) - [`get_activity_log`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-activity-log) — Activity Log (read) - [`get_webhook_delivery`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-delivery) — Get Webhook Delivery (read) - [`get_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-endpoint) — Get Webhook Endpoint (read) - [`list_webhook_deliveries`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries) — List Webhook Deliveries (read) - [`list_webhook_endpoints`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-endpoints) — List Webhook Endpoints (read) - [`pause_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/pause-webhook-endpoint) — Pause Webhook Endpoint (destructive) - [`resume_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/resume-webhook-endpoint) — Resume Webhook Endpoint (destructive) - [`retry_dead_webhook_deliveries`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries) — Retry Dead Webhook Deliveries (destructive) - [`retry_webhook_delivery`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-webhook-delivery) — Retry Webhook Delivery (destructive) - [`rotate_webhook_endpoint_secret`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/rotate-webhook-endpoint-secret) — Rotate Webhook Endpoint Secret (destructive) - [`test_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/test-webhook-endpoint) — Test Webhook Endpoint (destructive) --- # list_webhook_deliveries Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries The team's outbound webhook delivery log, newest first — what was posted, what the endpoint answered, and where each row is on the retry ladder. Every event Zuse Bifrost posts to one of the team's endpoints is a **delivery**: one row per endpoint per event, with the signed envelope that was sent, the target's response status and an excerpt of its body, and the row's position on the retry ladder. This is the application's delivery log over MCP, for seeing why a consumer rejected an event before deciding to [`retry_webhook_delivery`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-webhook-delivery) or [`retry_dead_webhook_deliveries`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries). > **The status tabs** > > `pending`: queued or waiting for its next attempt; `delivered`: the target > answered `2xx`; `failed`: the last attempt failed and the ladder has more > tries; `dead`: the ladder ran out, and only a retry brings it back. `all` is > the default. Anything else is `VALIDATION_FAILED`. - Requires ability: `webhook-delivery:view-any` - Runs the same action as [`GET /v1/webhook-deliveries`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/list-webhook-deliveries) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `all`, `pending`, `delivered`, `failed`, `dead` | no | Tab of the delivery log: all, pending, delivered, failed or dead. Defaults to all. | | `limit` | integer | no | Deliveries per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_webhook_deliveries", "arguments": { "status": "all", "limit": 1 } } ``` ## What it returns ```json { "data": [ { "id": "34f1d78e-05a2-4b69-8c3d-7e921ab06f45", "endpoint_id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "event": "project.status_changed", "event_id": "01HXZ3Q8M7Y2K4N6P9R1T3V5W7", "status": "failed", "attempts": 2, "response_status": 500, "response_excerpt": "upstream unavailable", "payload": { "id": "evt_01HXZ3Q8M7Y2K4N6P9R1T3V5W7", "type": "project.status_changed", "created_at": "2026-10-24T16:40:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "key": "CRM", "previous_status": "planning", "status": "in_development" } }, "next_attempt_at": "2026-10-24T16:50:00+00:00", "delivered_at": null, "dead_lettered_at": null, "created_at": "2026-10-24T16:40:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 312, "has_more": true } } ``` `payload` is the envelope exactly as the endpoint received it, so a consumer's bug can be reproduced from the log. `response_excerpt` is the first few kilobytes of the target's body, not the whole response. ## How it fails - `VALIDATION_FAILED` — status is not one of the five tabs, or limit/page is out of range. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-delivery:view-any. ## Example prompts > "Show me the dead-lettered webhook deliveries." > "Which deliveries failed in the last hour, and what did our endpoint answer?" > "List the pending deliveries; is the queue backing up?" ## Related - [get_webhook_delivery](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-delivery) — one row by id. - [retry_webhook_delivery](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-webhook-delivery) — replay one failed or dead row. - [retry_dead_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries) — replay everything dead-lettered since an instant. - [Webhook deliveries API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries) — the REST equivalent. - [Retries and delivery](https://alm-docs.labs.zuselab.dev/webhooks/v1/retries-and-delivery) — the ladder itself. --- # list_webhook_endpoints Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-endpoints List the outbound webhook endpoints registered for the token's team, paginated, with their health counters. Secrets are never returned. List the outbound webhook endpoints registered for the token's team, newest first, each with its target, the events it receives, whether it is active and the health counters the application shows. Useful for sanity-checking integrations after a suspected outage, before reading the delivery log. > **Note** > > Endpoint secrets are never returned by this tool. They are surfaced only at > the moment of creation or rotation, by > [`create_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/create-webhook-endpoint) > and > [`rotate_webhook_endpoint_secret`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/rotate-webhook-endpoint-secret). - Requires ability: `webhook-endpoint:view-any` - Runs the same action as [`GET /v1/webhook-endpoints`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/list-webhook-endpoints) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Endpoints per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_webhook_endpoints", "arguments": { "limit": 1, "page": 1 } } ``` ## What it returns ```json { "data": [ { "id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "name": "Delivery board", "url": "https://hooks.example.test/zusegenius", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "events": ["project.created", "project.status_changed"], "is_active": true, "disabled_at": null, "allowed_ips": null, "failure_count": 0, "consecutive_failures": 0, "last_success_at": "2026-10-25T10:05:00+00:00", "last_failure_at": null, "created_at": "2026-10-01T10:05:00+00:00", "updated_at": "2026-10-25T10:05:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 3, "has_more": false } } ``` ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-endpoint:view-any. ## Example prompts > "List our webhook endpoints." > "Which webhook endpoints have had failures recently, and which are paused?" ## Related - [get_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-endpoint) — one endpoint by id. - [list_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries) — why the counters look the way they do. - [Webhook endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) — the REST equivalent. - [Webhook security](https://alm-docs.labs.zuselab.dev/webhooks/v1/security) - [Testing and debugging](https://alm-docs.labs.zuselab.dev/webhooks/v1/testing-and-debugging) --- # pause_webhook_endpoint Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/pause-webhook-endpoint Stop deliveries to an endpoint without removing it. Events raised while paused are not queued for it. The application's on/off switch for an endpoint. A paused endpoint keeps its secret, its event subscriptions and its delivery log; what stops is new deliveries. Use it during a consumer outage or a migration, then [`resume_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/resume-webhook-endpoint). > **Paused events are not replayed** > > Events raised while the endpoint is paused are **not** queued for it, so they > are gone for that endpoint when it resumes. If the consumer must see > everything, leave the endpoint on and let the retry ladder and > [`retry_dead_webhook_deliveries`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries) > carry it through the outage instead. Only the person who registered the endpoint, or the team owner, may pause it; a fellow team member is refused with `FORBIDDEN`. Idempotent: an already-paused endpoint is a no-op. - Requires ability: `webhook-endpoint:update` - Runs the same action as [`POST /v1/webhook-endpoints/{endpoint}/pause`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/pause-a-webhook-endpoint) - Annotations: Destructive, Idempotent ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `endpoint_id` | string | yes | UUID of the webhook endpoint to pause. | ## Example call ```json { "name": "pause_webhook_endpoint", "arguments": { "endpoint_id": "231540dc-049d-4400-a42d-dc69ba798b74" } } ``` ## What it returns ```json { "data": { "id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "name": "Delivery board", "url": "https://hooks.example.test/zusegenius", "project_id": null, "events": ["project.created", "project.status_changed"], "is_active": false, "disabled_at": "2026-10-26T09:20:00+00:00", "allowed_ips": [], "failure_count": 2, "consecutive_failures": 0, "last_success_at": "2026-10-25T18:00:00+00:00", "last_failure_at": "2026-10-20T07:12:00+00:00", "created_at": "2026-10-01T10:05:00+00:00", "updated_at": "2026-10-26T09:20:00+00:00" }, "meta": {} } ``` ## How it fails - `FORBIDDEN` — the token belongs to a team member who did not register the endpoint and is not the team owner. - `RESOURCE_NOT_FOUND` — unknown endpoint_id, or an endpoint of another team. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-endpoint:update. ## Example prompts > "Pause webhook endpoint `c62a08f4-1b7d-4e35-9860-a37f5d21e0b9` while we migrate the board." > "Stop deliveries to the staging endpoint for now, don't delete it." > "Switch the delivery board webhook off until the handler is fixed." ## Related - [resume_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/resume-webhook-endpoint) — the reverse. - [delete_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/delete-webhook-endpoint) — when it is not coming back. - [Webhook endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) — the REST equivalent. --- # resume_webhook_endpoint Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/resume-webhook-endpoint Start deliveries to a paused endpoint again and clear its failure streak. Events raised while paused are not replayed. The reverse of [`pause_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/pause-webhook-endpoint), and also the way to bring back an endpoint the failure streak disabled. Resuming clears the consecutive-failure count, so a target that was fixed while paused is not disabled again on its first miss. > **Nothing is replayed on resume** > > Events raised while the endpoint was paused were never queued for it and are > not sent now. Rows that **dead-lettered before the pause** are a different > matter: replay those with > [`retry_dead_webhook_deliveries`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries) > once the consumer is healthy. Only the person who registered the endpoint, or the team owner, may resume it; a fellow team member is refused with `FORBIDDEN`. Idempotent: an already-active endpoint is a no-op. - Requires ability: `webhook-endpoint:update` - Runs the same action as [`POST /v1/webhook-endpoints/{endpoint}/resume`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/resume-a-webhook-endpoint) - Annotations: Destructive, Idempotent ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `endpoint_id` | string | yes | UUID of the webhook endpoint to resume. | ## Example call ```json { "name": "resume_webhook_endpoint", "arguments": { "endpoint_id": "231540dc-049d-4400-a42d-dc69ba798b74" } } ``` ## What it returns ```json { "data": { "id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "name": "Delivery board", "url": "https://hooks.example.test/zusegenius", "project_id": null, "events": ["project.created", "project.status_changed"], "is_active": true, "disabled_at": null, "allowed_ips": [], "failure_count": 2, "consecutive_failures": 0, "last_success_at": "2026-10-25T18:00:00+00:00", "last_failure_at": "2026-10-20T07:12:00+00:00", "created_at": "2026-10-01T10:05:00+00:00", "updated_at": "2026-10-26T11:00:00+00:00" }, "meta": {} } ``` ## How it fails - `FORBIDDEN` — the token belongs to a team member who did not register the endpoint and is not the team owner. - `RESOURCE_NOT_FOUND` — unknown endpoint_id, or an endpoint of another team. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-endpoint:update. ## Example prompts > "Resume webhook endpoint `c62a08f4-1b7d-4e35-9860-a37f5d21e0b9`; the board migration is done." > "Turn the delivery board webhook back on and replay whatever dead-lettered since Friday." > "The consumer is fixed; re-enable the endpoint the failure streak disabled." ## Related - [pause_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/pause-webhook-endpoint) — the reverse. - [retry_dead_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries) — replay what dead-lettered before the pause. - [Webhook endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) — the REST equivalent. --- # retry_dead_webhook_deliveries Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries Replay every webhook delivery the team dead-lettered since an instant, once the consumer is fixed. Defaults to the last 24 hours. The application's Replay button. After a consumer outage, every row that ran out of retries sits in the `dead` tab; this tool puts all of them since `since` back on the ladder in one call and answers with how many it retried. Omit `since` for the last 24 hours, the window the application uses. > **Every replayed row posts its event again** > > Confirm the consumer is healthy first, a test with > [`test_webhook_endpoint`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/test-webhook-endpoint) > is the cheap check, or the same rows dead-letter a second time. Only > dead-lettered rows are touched; failed rows still on the ladder retry on their > own. `since` may not be in the future. One call replays at most the number of rows `meta.limit` states; when `retried` equals it, call again for the rest. Repeating the call after a complete replay retries nothing, because the rows are no longer dead. - Requires ability: `webhook-delivery:retry` - Runs the same action as [`POST /v1/webhook-deliveries/retry-dead`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-every-recent-dead-delivery) - Annotations: Destructive, Open world ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `since` | string | no | ISO 8601 instant; only rows dead-lettered at or after it are replayed. Defaults to 24 hours ago. | ## Example call ```json { "name": "retry_dead_webhook_deliveries", "arguments": { "since": "<since>" } } ``` ## What it returns ```json { "data": { "retried": 17, "since": "2026-10-25T09:20:00+00:00" }, "meta": { "limit": 500 } } ``` `since` echoes the instant actually used, so a call with no argument shows the 24-hour boundary it applied. ## How it fails - `VALIDATION_FAILED` — since is malformed or in the future. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-delivery:retry. ## Example prompts > "Replay everything that dead-lettered since Friday 18:00; the board is back up." > "Retry all dead webhook deliveries from the last day." > "How many deliveries did the replay put back on the ladder?" ## Related - [retry_webhook_delivery](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-webhook-delivery) — one row at a time. - [list_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries) — see the dead tab before and after. - [resume_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/resume-webhook-endpoint) — a paused endpoint needs resuming first. - [Webhook deliveries API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries) — the REST equivalent. --- # retry_webhook_delivery Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-webhook-delivery Replay one failed or dead-lettered webhook delivery from the start of the retry ladder. A pending or delivered row is refused. Post the same event to the same endpoint again, once the consumer is fixed. The row goes back to `pending` with its attempt count reset and walks the retry ladder from the start; the returned row is that pending state. > **Only failed or dead rows** > > A `pending` row is already going to be posted, and a `delivered` row already > was; replaying either would post the event twice, so both are refused with > `VALIDATION_FAILED`. Read the row first with > [`get_webhook_delivery`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-delivery) if > you are not sure. - Requires ability: `webhook-delivery:retry` - Runs the same action as [`POST /v1/webhook-deliveries/{delivery}/retry`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries/retry-a-webhook-delivery) - Annotations: Destructive, Open world ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `delivery_id` | string | yes | UUID of the failed or dead-lettered delivery to replay. | ## Example call ```json { "name": "retry_webhook_delivery", "arguments": { "delivery_id": "f79655c3-045f-4800-a9a1-2c7afa3ac6e5" } } ``` ## What it returns ```json { "data": { "id": "34f1d78e-05a2-4b69-8c3d-7e921ab06f45", "endpoint_id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "event": "project.status_changed", "event_id": "01HXZ3Q8M7Y2K4N6P9R1T3V5W7", "status": "pending", "attempts": 0, "response_status": null, "response_excerpt": null, "payload": { "id": "evt_01HXZ3Q8M7Y2K4N6P9R1T3V5W7", "type": "project.status_changed", "created_at": "2026-10-24T16:40:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "key": "CRM", "previous_status": "planning", "status": "in_development" } }, "next_attempt_at": "2026-10-26T11:05:00+00:00", "delivered_at": null, "dead_lettered_at": null, "created_at": "2026-10-24T16:40:00+00:00" }, "meta": {} } ``` ## How it fails - `VALIDATION_FAILED` — the row is pending or delivered. - `RESOURCE_NOT_FOUND` — unknown delivery_id, or a delivery to another team's endpoint. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-delivery:retry. ## Example prompts > "Retry webhook delivery `34f1d78e-05a2-4b69-8c3d-7e921ab06f45`; the handler bug is fixed." > "Replay the dead-lettered `project.status_changed` for CRM to the board." > "Resend the failed delivery from this morning." ## Related - [retry_dead_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/retry-dead-webhook-deliveries) — every dead-lettered row since an instant, in one call. - [get_webhook_delivery](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-delivery) — read the row first. - [Webhook deliveries API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-deliveries) — the REST equivalent. --- # rotate_webhook_endpoint_secret Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/rotate-webhook-endpoint-secret Replace an endpoint's signing secret. The new secret is returned in this response only and the old one stops verifying at once. Mint a new signing secret for an endpoint, because the old one leaked, because the consumer lost it, or on a rotation schedule. From this call on, every delivery to the endpoint is signed with the new secret and the previous one stops verifying. > **Destructive, and the secret is returned once** > > Deliveries signed with the new secret start immediately, so a consumer still > checking the old one rejects everything until it is updated. Confirm the > `endpoint_id` with a person, have the new `secret` deployed straight from this > response, and never call it "to check": every call mints another secret, and > none of them can be read back. Only the person who registered the endpoint, or the team owner, may rotate; a fellow team member is refused with `FORBIDDEN`. Not idempotent. - Requires ability: `webhook-endpoint:update` - Runs the same action as [`POST /v1/webhook-endpoints/{endpoint}/rotate-secret`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/rotate-a-webhook-endpoints-secret) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `endpoint_id` | string | yes | UUID of the webhook endpoint whose secret to rotate. | ## Example call ```json { "name": "rotate_webhook_endpoint_secret", "arguments": { "endpoint_id": "231540dc-049d-4400-a42d-dc69ba798b74" } } ``` ## What it returns ```json { "data": { "id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "name": "Delivery board", "url": "https://hooks.example.test/zusegenius", "project_id": null, "events": ["project.created", "project.status_changed"], "is_active": true, "disabled_at": null, "allowed_ips": [], "failure_count": 2, "consecutive_failures": 0, "last_success_at": "2026-10-25T18:00:00+00:00", "last_failure_at": "2026-10-20T07:12:00+00:00", "created_at": "2026-10-01T10:05:00+00:00", "updated_at": "2026-10-26T11:00:00+00:00", "secret": "whsec_..." }, "meta": {} } ``` ## How it fails - `FORBIDDEN` — the token belongs to a team member who did not register the endpoint and is not the team owner. - `RESOURCE_NOT_FOUND` — unknown endpoint_id, or an endpoint of another team. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-endpoint:update. ## Example prompts > "Rotate the signing secret on webhook endpoint `c62a08f4-1b7d-4e35-9860-a37f5d21e0b9`; it was pasted into a public channel." (confirm first)" > "Mint a new secret for the delivery board endpoint; I'll update the handler now." > "Quarterly rotation for the delivery board webhook." ## Related - [test_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/test-webhook-endpoint) — confirm the consumer verifies the new secret. - [create_webhook_endpoint](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/create-webhook-endpoint) — the other place a secret is ever shown. - [Webhook endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) — the REST equivalent. - [Signature verification](https://alm-docs.labs.zuselab.dev/webhooks/v1/signature-verification) — how the signature is computed. --- # test_webhook_endpoint Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/test-webhook-endpoint Queue a synthetic test event to an endpoint so its consumer can be checked end to end. Returns the queued delivery to poll. Send an endpoint a signed test event without waiting for something real to happen. The answer is the queued delivery in the [`get_webhook_delivery`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-delivery) row shape, status `pending`; poll that tool for the target's `response_status` and `response_excerpt` once the worker has posted it. > **Same worker, same signature, same ladder** > > The test delivery is posted by the same job that posts real events, signed > with the endpoint's current secret, and retried on the same ladder if the > target fails. Its envelope is a `project.created` whose `data` is a marker > (`synthetic: true` and the endpoint's id), never a realistic event, so a > consumer can tell it apart. A consumer that verifies the signature and returns > `2xx` for the test will do so for real events. Only the person who registered the endpoint, or the team owner, may fire a test; a fellow team member is refused with `FORBIDDEN`. The REST equivalent rate-limits the same action per token. - Requires ability: `webhook-endpoint:update` - Runs the same action as [`POST /v1/webhook-endpoints/{endpoint}/test`](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints/test-fire-a-webhook-endpoint) - Annotations: Destructive, Open world ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `endpoint_id` | string | yes | UUID of the webhook endpoint to send a test event to. | ## Example call ```json { "name": "test_webhook_endpoint", "arguments": { "endpoint_id": "231540dc-049d-4400-a42d-dc69ba798b74" } } ``` ## What it returns ```json { "data": { "id": "34f1d78e-05a2-4b69-8c3d-7e921ab06f45", "endpoint_id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9", "event": "project.created", "event_id": "01HXZ3Q8M7Y2K4N6P9R1T3V5W7", "status": "pending", "attempts": 0, "response_status": null, "response_excerpt": null, "payload": { "id": "evt_01HXZ3Q8M7Y2K4N6P9R1T3V5W7", "type": "project.created", "created_at": "2026-10-26T09:20:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "synthetic": true, "endpoint_id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9" } }, "next_attempt_at": null, "delivered_at": null, "dead_lettered_at": null, "created_at": "2026-10-26T09:20:00+00:00" }, "meta": {} } ``` ## How it fails - `FORBIDDEN` — the token belongs to a team member who did not register the endpoint and is not the team owner. - `RESOURCE_NOT_FOUND` — unknown endpoint_id, or an endpoint of another team. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks webhook-endpoint:update. ## Example prompts > "Send a test event to webhook endpoint `c62a08f4-1b7d-4e35-9860-a37f5d21e0b9` and tell me what it answered." > "We just deployed the handler; fire a test at the delivery board endpoint." > "Check the delivery board webhook is still accepting deliveries." ## Related - [get_webhook_delivery](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-webhook-delivery) — poll the returned id for the result. - [list_webhook_deliveries](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/list-webhook-deliveries) — the whole log. - [Webhook endpoints API](https://alm-docs.labs.zuselab.dev/api/v1/reference/webhook-endpoints) — the REST equivalent. - [Testing and debugging webhooks](https://alm-docs.labs.zuselab.dev/webhooks/v1/testing-and-debugging) --- # create_persona Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/create-persona Adds one of the people the product is built for, so the stories that follow have someone to be written as. Add a persona to a project: `project_id` and `name` are required; `tagline`, `role_title`, `demographics`, `goals`, `frustrations`, `behaviours` (each HTML), `tech_proficiency` (`low`, `medium` or `high`; `medium` by default), `devices` (short tags) and `quote` fill in the rest. The persona is written through the same action the People page and the intake wizard run, and `persona.created` goes out. Write a persona for each kind of person the brief names before the stories that speak as them, so `create_work_item` can name the persona and the "as a" reads the same everywhere. A staff token with `persona:create` on the project is needed. - Requires ability: `persona:create` - Runs the same action as [`POST /v1/projects/{project}/personas`](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/create-a-persona) - Fires events: [`persona.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `name` | string | yes | What the persona is called, 2 to 80 characters. | | `tagline` | string | no | One line that sums them up. | | `role_title` | string | no | What they do, in their own world. | | `demographics` | string | no | Who they are, as HTML. | | `goals` | string | no | What they want from the product, as HTML. | | `frustrations` | string | no | What gets in their way today, as HTML. | | `behaviours` | string | no | How they work and what they reach for, as HTML. | | `tech_proficiency` | `low`, `medium`, `high` | no | How at ease they are with technology; medium by default. | | `devices` | array of string | no | The devices they use, as short tags. | | `quote` | string | no | Something they would say. | ## Example call ```json { "name": "create_persona", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Busy parent", "tagline": "Drops laundry on the way to work and wants it back by Friday", "role_title": "Working parent of two", "demographics": null, "goals": "<p>Book a pickup in under a minute and know when the laundry is ready.</p>", "frustrations": "<p>Calling the shop to ask whether the order is done.</p>", "behaviours": null, "tech_proficiency": "medium", "devices": ["phone"], "quote": "Just tell me when it is ready.", "position": 1, "archived_at": null, "created_at": "2026-10-28T09:25:00+00:00", "updated_at": "2026-10-28T09:25:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — name missing or outside 2 to 80 characters, a tech_proficiency outside the three, more devices than allowed, or a name another persona of the project already uses. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry persona:create. - `TOKEN_MISSING_ABILITY` — token lacks persona:create. ## Example prompts > "Add the personas for the laundry shop: a busy parent, a hostel student and the shop manager." > "Create a persona for the delivery driver who uses the app on a phone in the van." > "Write a persona for an office manager ordering for the whole team." ## Related - [list_personas](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas) — who the product is built for today. - [update_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-persona) — change a persona later. - [create_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-work-item) — the stories written as a persona. - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas) — the same door over REST. --- # create_stakeholder Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/create-stakeholder Adds one of the people a project answers to, so the sponsor and the product owner a brief names take their seats as the conversation goes. Add a stakeholder to a project: `project_id` and `name` are required; `email`, `phone`, `organisation`, `side` (`client` by default, or `zuse`, `third_party`), `title`, `role` (`sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations` or `other`), `influence` and `interest` (`low`, `medium`, `high`), `raci_default`, the approval flags `approves_requirements`, `signs_off_uat`, `approves_releases` and `receives_status_reports` (on by default), `notes` (HTML) and `user_id` (the guest account they sign in with, when linked) fill in the rest. The stakeholder is written through the same action the People page and the intake wizard run, and `stakeholder.created` goes out. A seat with `approves_requirements` is what an approval policy names when the client signs off requirements; the seat decides only once an account is linked to it. A staff token with `stakeholder:create` on the project is needed. - Requires ability: `stakeholder:create` - Runs the same action as [`POST /v1/projects/{project}/stakeholders`](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/add-a-stakeholder) - Fires events: [`stakeholder.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `name` | string | yes | The person's name, 2 to 120 characters. | | `email` | string | no | Where to reach them. | | `phone` | string | no | A phone number. | | `organisation` | string | no | The organisation they belong to, in words. | | `side` | `client`, `zuse`, `third_party` | no | Whose side they speak for; client by default. | | `title` | string | no | Their job title. | | `role` | `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations`, `other` | no | What they do for the project; other by default. | | `influence` | `low`, `medium`, `high` | no | How much weight their word carries; medium by default. | | `interest` | `low`, `medium`, `high` | no | How closely they follow the project; medium by default. | | `raci_default` | `responsible`, `accountable`, `consulted`, `informed` | no | Their default RACI letter; null for none. | | `approves_requirements` | boolean | no | Whether they are asked to approve requirements. | | `signs_off_uat` | boolean | no | Whether they sign off UAT runs. | | `approves_releases` | boolean | no | Whether they approve releases. | | `receives_status_reports` | boolean | no | Whether status reports go to them; on by default. | | `notes` | string | no | Anything the team should know, as HTML. | | `user_id` | string | no | UUID of the guest account they sign in with, when linked. | ## Example call ```json { "name": "create_stakeholder", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "user_id": null, "name": "Nadeesha Perera", "email": "nadeesha@example.test", "phone": null, "organisation": "Super Clean Laundry", "side": "client", "title": "Owner", "role": "product_owner", "influence": "high", "interest": "high", "raci_default": "accountable", "approves_requirements": true, "signs_off_uat": true, "approves_releases": false, "receives_status_reports": true, "notes": null, "position": 1, "created_at": "2026-10-28T09:22:00+00:00", "updated_at": "2026-10-28T09:22:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — name missing or outside 2 to 120 characters, an email that is not an address, a side, role, influence, interest or raci_default outside its set, or a user_id that is not an account the project can link. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry stakeholder:create. - `TOKEN_MISSING_ABILITY` — token lacks stakeholder:create. ## Example prompts > "Nadeesha Perera owns the shop and will sign off the requirements; add her as the product owner." > "Add our finance contact at the client, Ruwan Silva, who approves the budget." > "Seat the shop manager as the end-user representative." ## Related - [list_stakeholders](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders) — who is seated today. - [update_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-stakeholder) — change a seat later. - [update_approval_policy](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-approval-policy) — name the seat on an approval. - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders) — the same door over REST. --- # delete_persona Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-persona Deletes a persona of a project after the person confirms; the stories and use cases written as them keep their words. Delete a persona by `persona_id`. The stories and use cases written as the persona keep their words and lose the link. The removal goes through the same action the People page runs and `persona.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. A staff token with `persona:delete` on the project is needed. - Requires ability: `persona:delete` - Runs the same action as [`DELETE /v1/personas/{persona}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/delete-a-persona) - Fires events: [`persona.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `persona_id` | string | yes | UUID of the persona to delete. | ## Example call ```json { "name": "delete_persona", "arguments": { "persona_id": "6526f189-ca85-4000-a4ee-77d0c470726f" } } ``` ## What it returns ```json { "data": { "persona_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown persona_id, or a persona outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry persona:delete. - `TOKEN_MISSING_ABILITY` — token lacks persona:delete. ## Example prompts > "Delete the office manager persona; the client dropped business orders." > "Remove the duplicate student persona." ## Related - [update_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-persona) — change it instead. - [list_personas](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas) — who is left. - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas) — the same door over REST. --- # delete_stakeholder Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-stakeholder Deletes a stakeholder of a project after the person confirms; approvals that name the seat lose an approver. Delete a stakeholder by `stakeholder_id`. Approvals whose policy names the seat lose an approver; requests already open keep the approvers they were sent to. The removal goes through the same action the People page runs and `stakeholder.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. A staff token with `stakeholder:delete` on the project is needed. - Requires ability: `stakeholder:delete` - Runs the same action as [`DELETE /v1/stakeholders/{stakeholder}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/remove-a-stakeholder) - Fires events: [`stakeholder.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `stakeholder_id` | string | yes | UUID of the stakeholder to delete. | ## Example call ```json { "name": "delete_stakeholder", "arguments": { "stakeholder_id": "d6255469-a544-4000-a812-2dd00c1df08f" } } ``` ## What it returns ```json { "data": { "stakeholder_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown stakeholder_id, or a stakeholder outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry stakeholder:delete. - `TOKEN_MISSING_ABILITY` — token lacks stakeholder:delete. ## Example prompts > "Remove the old marketing contact; she left the client." > "Delete the duplicate seat for Ruwan." ## Related - [update_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-stakeholder) — change the seat instead. - [list_stakeholders](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders) — who is left. - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders) — the same door over REST. --- # get_persona Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-persona Fetch a single persona by UUID — their sketch in full, with goals, frustrations, behaviours, proficiency, devices, quote and archive stamp. Fetch one persona in full once [`list_personas`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas) has named it, or once a story's `persona_id` has: the whole sketch the analyst wrote, to keep in view while drafting the story, the acceptance criteria or the test that serves this person. A persona outside the token's team or project scope, or one the person cannot see, answers `RESOURCE_NOT_FOUND`, never a permission error, so existence does not leak. - Requires ability: `persona:view` - Runs the same action as [`GET /v1/personas/{persona}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/get-a-persona) - Runs the same action as [`GET /v1/personas/{persona}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-a-personas-versions) - Runs the same action as [`GET /v1/personas/{persona}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/read-one-version-of-a-persona) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `persona_id` | string | yes | UUID of the persona to fetch. | ## Example call ```json { "name": "get_persona", "arguments": { "persona_id": "6526f189-ca85-4000-a4ee-77d0c470726f" } } ``` ## What it returns ```json { "data": { "id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d12", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Busy Buyer", "tagline": "Orders for three branches between meetings.", "role_title": "Procurement officer", "demographics": "<p>Mid-forties, twelve years at the company, works from the regional office.</p>", "goals": "<ul><li>Reorder last month's basket in two minutes.</li></ul>", "frustrations": "<ul><li>Has to call support to change a delivery address.</li></ul>", "behaviours": "<p>Works on a laptop at the desk and a phone on the warehouse floor.</p>", "tech_proficiency": "medium", "devices": ["Laptop", "Phone"], "quote": "Just let me repeat last month's order.", "position": 0, "archived_at": null, "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" }, "meta": {} } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown persona_id, or a persona outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks persona:view. ## Example prompts > "Get persona `b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d12`." > "What frustrates the Busy Buyer today, and which devices does she use?" ## Related - [list_personas](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas) - [get_activity_log](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-activity-log) — the persona's history. - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas) --- # get_stakeholder Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-stakeholder Fetch a single stakeholder by UUID — their side, role, weight, RACI default, approver flags and, for a staff token, contact details, notes and linked account. Fetch one stakeholder in full once [`list_stakeholders`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders) has named them: whose side they speak for, what they do for the project, which approvals and reports name them, and for a staff token how to reach them and what the team noted about them. A stakeholder outside the token's team or project scope, or one the person cannot see, answers `RESOURCE_NOT_FOUND`, never a permission error, so existence does not leak. - Requires ability: `stakeholder:view` - Runs the same action as [`GET /v1/stakeholders/{stakeholder}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/get-a-stakeholder) - Runs the same action as [`GET /v1/stakeholders/{stakeholder}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-a-stakeholders-versions) - Runs the same action as [`GET /v1/stakeholders/{stakeholder}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/read-one-version-of-a-stakeholder) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `stakeholder_id` | string | yes | UUID of the stakeholder to fetch. | ## Example call ```json { "name": "get_stakeholder", "arguments": { "stakeholder_id": "d6255469-a544-4000-a812-2dd00c1df08f" } } ``` ## What it returns ```json { "data": { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "user_id": "6b2d9f41-0c7e-4a58-9d3f-1e8a7c5b2d90", "name": "Jane Perera", "email": "jane@acme.example", "phone": "+94 77 123 4567", "organisation": "Acme Retail", "side": "client", "title": "Head of Digital", "role": "product_owner", "influence": "high", "interest": "high", "raci_default": "accountable", "approves_requirements": true, "signs_off_uat": true, "approves_releases": true, "receives_status_reports": true, "notes": "<p>Prefers a call over email for anything urgent.</p>", "position": 0, "created_at": "2026-10-20T09:10:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" }, "meta": {} } ``` A guest's token receives the same record without `email`, `phone`, `influence`, `interest`, `raci_default`, `notes` and `user_id`. ## How it fails - `RESOURCE_NOT_FOUND` — unknown stakeholder_id, or a stakeholder outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks stakeholder:view. ## Example prompts > "Get stakeholder `a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01`." > "What does Jane Perera decide on this project, and how do we reach her?" ## Related - [list_stakeholders](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders) - [get_activity_log](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-activity-log) — the stakeholder's history. - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders) --- # People Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people The people of a project: the stakeholders it answers to (the client's sponsor, product owner and approvers, Zuse's own leads, third parties, each with a side, a role, a weight and the approvals that name them) and the personas it is built for (named, sketched users with goals, frustrations and habits, the "as a …" of every story). These tools read both as the token sees them, a guest's token without the stakeholders' contact details, ratings and notes; and, for a staff token, add and change both (`create_stakeholder`, `update_stakeholder`, `create_persona`, `update_persona`) and remove one after a confirmation; an approval policy names the stakeholder seats through `update_approval_policy`. ## Tools - [`create_persona`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/create-persona) — Create Persona (destructive) - [`create_stakeholder`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/create-stakeholder) — Create Stakeholder (destructive) - [`delete_persona`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-persona) — Delete Persona (destructive) - [`delete_stakeholder`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-stakeholder) — Delete Stakeholder (destructive) - [`get_persona`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-persona) — Get Persona (read) - [`get_stakeholder`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-stakeholder) — Get Stakeholder (read) - [`list_personas`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas) — List Personas (read) - [`list_stakeholders`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders) — List Stakeholders (read) - [`update_persona`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-persona) — Update Persona (destructive) - [`update_stakeholder`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-stakeholder) — Update Stakeholder (destructive) --- # list_personas Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas Paginated list of the archetypal users a project is built for, with their goals, frustrations, habits, proficiency and devices, narrowed by search words or archived state. Return the personas of one project in the project's order: the named, sketched users the analyst wrote the product for, each with a tagline, a role title, four prose fields (demographics, goals, frustrations, behaviours), a tech proficiency, the devices they use and a quote. Read it before drafting a story, a use case or a test, so you write for the people the analyst sketched rather than a generic user, and to find the `persona_id` a story will name. Narrow with `search` (words in the name, the tagline or the role title) and `archived` (`true` for archived personas only, `false` for live ones; omit for both). - Requires ability: `persona:view-any` - Runs the same action as [`GET /v1/projects/{project}/personas`](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/list-personas) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose personas to list. | | `search` | string | no | Words the name, the tagline or the role title must contain. | | `archived` | boolean | no | `true` lists archived personas only, `false` live ones only; omit for both. | | `limit` | integer | no | Personas per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_personas", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d12", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Busy Buyer", "tagline": "Orders for three branches between meetings.", "role_title": "Procurement officer", "demographics": "<p>Mid-forties, twelve years at the company, works from the regional office.</p>", "goals": "<ul><li>Reorder last month's basket in two minutes.</li></ul>", "frustrations": "<ul><li>Has to call support to change a delivery address.</li></ul>", "behaviours": "<p>Works on a laptop at the desk and a phone on the warehouse floor.</p>", "tech_proficiency": "medium", "devices": ["Laptop", "Phone"], "quote": "Just let me repeat last month's order.", "position": 0, "archived_at": null, "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks persona:view-any. ## Example prompts > "Who is the Customer Portal built for?" > "List the personas of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` with their goals and frustrations." > "Which persona would the reorder story be written for?" ## Related - [get_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-persona) - [list_stakeholders](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders) — who the project answers to, as opposed to who it is built for. - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas) --- # list_stakeholders Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders Paginated list of the people a project answers to, with their side, role, weight and the approvals that name them, narrowed by side, role, approval or search words. Return the stakeholders of one project in the project's order: who they are, whose side they speak for (`client`, `zuse` or `third_party`), what they do for the project, how much weight their word carries, their default RACI letter and which approvals and reports name them. Read it before drafting an approval request, a status report or a question for the client, so you address the person who decides rather than a generic contact. Narrow with `side`, `role` (`sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations`, `other`), `approves` (`requirements`, `uat` or `releases`: the people that approval names) and `search` (words in the name, the email, the organisation or the title). A client guest's token receives each row without `email`, `phone`, `influence`, `interest`, `raci_default`, `notes` and `user_id`. - Requires ability: `stakeholder:view-any` - Runs the same action as [`GET /v1/projects/{project}/stakeholders`](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/list-stakeholders) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose stakeholders to list. | | `side` | `client`, `zuse`, `third_party` | no | Narrow to one side. | | `role` | `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations`, `other` | no | Narrow to one role. | | `approves` | `requirements`, `uat`, `releases` | no | Narrow to the people an approval names: `requirements`, `uat` or `releases`. | | `search` | string | no | Words the name, the email, the organisation or the title must contain. | | `limit` | integer | no | Stakeholders per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_stakeholders", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "user_id": null, "name": "Jane Perera", "email": "jane@acme.example", "phone": "+94 77 123 4567", "organisation": "Acme Retail", "side": "client", "title": "Head of Digital", "role": "product_owner", "influence": "high", "interest": "high", "raci_default": "accountable", "approves_requirements": true, "signs_off_uat": true, "approves_releases": false, "receives_status_reports": true, "notes": "<p>Prefers a call over email for anything urgent.</p>", "position": 0, "created_at": "2026-10-20T09:10:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — side or role is not one of its values, or approves is not requirements, uat or releases. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks stakeholder:view-any. ## Example prompts > "Who signs off requirements on the Customer Portal project?" > "List the client-side stakeholders of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` with their roles." > "Who should I ask about the invoicing rules on this project?" ## Related - [get_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-stakeholder) - [list_personas](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas) — who the project is built for, as opposed to who it answers to. - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders) --- # update_persona Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-persona Changes a persona of a project in part; the devices replace the list when sent. Change a persona by `persona_id` with any of `name`, `tagline`, `role_title`, `demographics`, `goals`, `frustrations`, `behaviours` (each HTML), `tech_proficiency`, `devices` (the list replaces the current one) and `quote`. A field left out keeps its value; a field sent as null is cleared. The change goes through the same action the People page runs and `persona.updated` goes out with what moved. A staff token with `persona:update` on the project is needed. - Requires ability: `persona:update` - Runs the same action as [`PATCH /v1/personas/{persona}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/update-a-persona) - Fires events: [`persona.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `persona_id` | string | yes | UUID of the persona. | | `name` | string | no | What the persona is called, 2 to 80 characters. | | `tagline` | string | no | One line that sums them up. | | `role_title` | string | no | What they do, in their own world. | | `demographics` | string | no | Who they are, as HTML. | | `goals` | string | no | What they want from the product, as HTML. | | `frustrations` | string | no | What gets in their way today, as HTML. | | `behaviours` | string | no | How they work and what they reach for, as HTML. | | `tech_proficiency` | `low`, `medium`, `high` | no | How at ease they are with technology; medium by default. | | `devices` | array of string | no | The devices they use, as short tags. | | `quote` | string | no | Something they would say. | ## Example call ```json { "name": "update_persona", "arguments": { "persona_id": "6526f189-ca85-4000-a4ee-77d0c470726f" } } ``` ## What it returns ```json { "data": { "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Busy parent", "tagline": "Drops laundry on the way to work and wants it back by Friday", "role_title": "Working parent of two", "demographics": "<p>30 to 45, two children, both parents working.</p>", "goals": "<p>Book a pickup in under a minute and know when the laundry is ready.</p>", "frustrations": "<p>Calling the shop to ask whether the order is done.</p>", "behaviours": "<p>Orders from the phone in the evening; pays by card.</p>", "tech_proficiency": "medium", "devices": ["phone", "laptop"], "quote": "Just tell me when it is ready.", "position": 1, "archived_at": null, "created_at": "2026-10-28T09:25:00+00:00", "updated_at": "2026-10-28T14:00:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a tech_proficiency outside the three, or a name another persona of the project already uses. - `RESOURCE_NOT_FOUND` — unknown persona_id, or a persona outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry persona:update. - `TOKEN_MISSING_ABILITY` — token lacks persona:update. ## Example prompts > "Add that the busy parent also uses a laptop in the evening." > "Rewrite the student persona's frustrations: they share a machine with six others." > "Set the driver's tech proficiency to low." ## Related - [get_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-persona) — the persona as it stands. - [delete_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-persona) — take it away. - [Personas API](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas) — the same door over REST. --- # update_stakeholder Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/update-stakeholder Changes a stakeholder of a project in part, so an approval flag or an email can be added as the person mentions it. Change a stakeholder by `stakeholder_id` with any of `name`, `email`, `phone`, `organisation`, `side`, `title`, `role`, `influence`, `interest`, `raci_default`, `approves_requirements`, `signs_off_uat`, `approves_releases`, `receives_status_reports`, `notes` (HTML) and `user_id`. A field left out keeps its value; a field sent as null is cleared. The change goes through the same action the People page runs and `stakeholder.updated` goes out with what moved. A staff token with `stakeholder:update` on the project is needed. - Requires ability: `stakeholder:update` - Runs the same action as [`PATCH /v1/stakeholders/{stakeholder}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/update-a-stakeholder) - Fires events: [`stakeholder.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `stakeholder_id` | string | yes | UUID of the stakeholder. | | `name` | string | no | The person's name, 2 to 120 characters. | | `email` | string | no | Where to reach them. | | `phone` | string | no | A phone number. | | `organisation` | string | no | The organisation they belong to, in words. | | `side` | `client`, `zuse`, `third_party` | no | Whose side they speak for; client by default. | | `title` | string | no | Their job title. | | `role` | `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations`, `other` | no | What they do for the project; other by default. | | `influence` | `low`, `medium`, `high` | no | How much weight their word carries; medium by default. | | `interest` | `low`, `medium`, `high` | no | How closely they follow the project; medium by default. | | `raci_default` | `responsible`, `accountable`, `consulted`, `informed` | no | Their default RACI letter; null for none. | | `approves_requirements` | boolean | no | Whether they are asked to approve requirements. | | `signs_off_uat` | boolean | no | Whether they sign off UAT runs. | | `approves_releases` | boolean | no | Whether they approve releases. | | `receives_status_reports` | boolean | no | Whether status reports go to them; on by default. | | `notes` | string | no | Anything the team should know, as HTML. | | `user_id` | string | no | UUID of the guest account they sign in with, when linked. | ## Example call ```json { "name": "update_stakeholder", "arguments": { "stakeholder_id": "d6255469-a544-4000-a812-2dd00c1df08f" } } ``` ## What it returns ```json { "data": { "id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "user_id": "9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a", "name": "Nadeesha Perera", "email": "nadeesha@example.test", "phone": "+94 77 123 4567", "organisation": "Super Clean Laundry", "side": "client", "title": "Owner", "role": "product_owner", "influence": "high", "interest": "high", "raci_default": "accountable", "approves_requirements": true, "signs_off_uat": true, "approves_releases": true, "receives_status_reports": true, "notes": null, "position": 1, "created_at": "2026-10-28T09:22:00+00:00", "updated_at": "2026-10-29T08:10:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a value outside its set, or a user_id that is not an account the project can link. - `RESOURCE_NOT_FOUND` — unknown stakeholder_id, or a stakeholder outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry stakeholder:update. - `TOKEN_MISSING_ABILITY` — token lacks stakeholder:update. ## Example prompts > "Nadeesha also approves releases; update her seat." > "Link Nadeesha's seat to the guest account she accepted yesterday." > "Change Ruwan's role to finance and lower his interest to medium." ## Related - [get_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-stakeholder) — the seat as it stands. - [delete_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/delete-stakeholder) — take the seat away. - [Stakeholders API](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders) — the same door over REST. --- # analyse_change_impact Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/analyse-change-impact Asks Heimdall to read one change request against its baseline and the records it names, writes the draft onto the request, fills the impact fields the analyst left empty, and answers with the request; one metered call under the team's AI budget. Get Heimdall's reading of a change request's impact. Heimdall is given the request as raised, the impact already written, the baseline it changes with every record held, and each record the request names in full with what links to it, and answers with a summary, the scope and the risk as prose, a schedule in days, an effort in hours, a level, the records it believes the request forgot and the questions to ask before deciding. The whole reading is kept on the request as `ai_impact_draft` with `analysed_at`; the request's own fields are filled only where they were empty (the level, the schedule, the effort, the scope, the risk), and a number the analyst wrote is never replaced. A request still only `submitted` moves `under_analysis`. The call is metered like a turn in the Heimdall panel: the budget is asked for room first and the usage is filed as impact analysis, and `change_request.analysed` goes out. Heimdall estimates; the people decide, and nothing is submitted or unlocked by this call. - Requires ability: `change-request:analyse` - Runs the same action as [`POST /v1/change-requests/{changeRequest}/analyse`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/analyse-a-change-requests-impact) - Fires events: [`change_request.analysed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-analysed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `change_request_id` | string | yes | UUID of the change request to analyse. | ## Example call ```json { "name": "analyse_change_impact", "arguments": { "change_request_id": "d2589e11-c5a2-4000-ac6f-e7a8e29873a7" } } ``` ## What it returns ```json { "data": { "id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 2, "reference": "CRM-CR-2", "title": "Accept vouchers on subscriptions too", "description": "<p>The client wants the voucher applied to the first renewal as well.</p>", "reason": "client_request", "requested_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "requester_name": "Bea Analyst", "stakeholder_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "stakeholder_name": "Gail of the Client", "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "baseline_name": "v1.0 scope", "affected": [ { "artefact_type": "requirement", "artefact_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Password reset by email" } ], "impact_scope": "<p><strong>CRM-FR-12</strong> gains a renewal clause; the story that implements it and its tests change.</p>", "impact_schedule_days": 3, "impact_effort_hours": "12.50", "impact_cost": null, "impact_risk": "<p>The renewal job runs unattended; a wrong template confuses rather than leaks.</p>", "impact_level": "medium", "ai_impact_draft": "<p>A small change to the renewal flow; the story that implements the requirement changes with it.</p><h4>Scope</h4><p><strong>CRM-FR-12</strong> gains a renewal clause; the story that implements it and its tests change.</p><h4>Risk</h4><p>The renewal job runs unattended; a wrong template confuses rather than leaks.</p><h4>Records touched</h4><ul><li><strong>CRM-142</strong> Apply a voucher at checkout: implements the requirement and changes with it.</li></ul><h4>To ask before deciding</h4><ul><li>Does the renewal email carry the voucher as well?</li></ul>", "analysed_at": "2026-10-29T10:40:00+00:00", "priority": "high", "status": "under_analysis", "decision_note": null, "verified_by": null, "verified_at": null, "version": 0, "approved_at": null, "approved_revision_id": null, "position": 1, "created_at": "2026-10-29T09:20:00+00:00", "updated_at": "2026-10-29T10:40:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — the provider did not answer; nothing was written and nothing filed. - `AI_BUDGET_EXHAUSTED` — the team's monthly AI budget has no room; a team admin raises it under Settings → AI. - `RESOURCE_NOT_FOUND` — unknown change_request_id, or a request outside the token's scope. - `FORBIDDEN` — the token is a client guest's, the holder's role on the project does not carry change-request:analyse or ai:use, or the project's assists are off. - `TOKEN_MISSING_ABILITY` — token lacks change-request:analyse. ## Example prompts > "Work out the impact of CRM-CR-2 and tell me what it touches beyond the requirement it names." > "Analyse change request `c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f` and list the questions I should put to the client." > "Read the voucher change again now that I wrote the scope; where do you differ?" ## Related - [get_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-change-request) — read the draft and the fields it filled. - [update_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-change-request) — keep or change the numbers Heimdall proposed. - [convene_council](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — the same request read by several voices on the change impact moment. - [Change Requests API](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests) — the same reading over REST. --- # comment_on_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/comment-on-item Post a comment on one artefact as the person the token acts as, in Markdown, under a first comment when answering; the people named and the followers are told and comment.created goes out. Post a comment on one artefact of a project: `project_id`, `artefact_type` (`epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`), `artefact_id`, `body` in Markdown (turned into the same sanitised HTML the editor writes), an optional `parent_id` to answer the first comment of a thread, and `internal` to keep the comment from client guests, which needs `comment:internal`. Name people with `@` and their full name to tell them; the artefact's followers are told either way, and the person the token acts as becomes a follower. Comment when you have a question, a finding or a decision to record against the artefact: before you move a status, after you link a pull request, when a criterion cannot be verified. A comment is a claim on the record, so say what you did and why rather than what you plan. Editing, resolving and deleting comments stays with people: correct yourself with a new comment. - Requires ability: `comment:create` - Runs the same action as [`POST /v1/projects/{project}/comments`](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/post-a-comment) - Fires events: [`comment.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-created) ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project the artefact belongs to. | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type. | | `artefact_id` | string | yes | UUID of the artefact to comment on. | | `body` | string | yes | The comment, in Markdown; up to 20,000 characters. | | `internal` | boolean | no | Keep the comment from client guests; needs comment:internal. False by default. | | `parent_id` | string | no | UUID of the first comment of the thread to answer; omit for a new thread. | ## Example call ```json { "name": "comment_on_item", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "artefact_type": "epic", "artefact_id": "5683b5a5-ee5e-4400-a374-b33e4e067dc7", "body": "<body>" } } ``` ## What it returns ```json { "data": { "id": "f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f7a8b92", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "artefact_type": "work-item", "artefact_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "parent_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "author_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "author_name": "Sam Lee", "body": "<p><strong>Confirmed</strong> by CRM-BR-7: the voucher comes last.</p>", "internal": false, "mentions": [], "resolved": false, "resolved_at": null, "resolved_by": null, "edited_at": null, "created_at": "2026-10-28T10:05:00+00:00", "updated_at": "2026-10-28T10:05:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a missing or unknown type or id, an empty body or one over 20,000 characters, or a parent_id that is missing, a reply itself or on another artefact. - `RESOURCE_NOT_FOUND` — unknown project_id, or an artefact the token may not see. - `FORBIDDEN` — the token holder's role on the project does not carry comment:create; a guest token on a project whose guests may not comment; internal without comment:internal. - `TOKEN_MISSING_ABILITY` — token lacks comment:create. ## Example prompts > "Comment on CRM-142 that the voucher rule is applied last, per CRM-BR-7, and ask @Jane Doe to update the criteria." > "Answer the open question on requirement `3a4b5c6d-7e8f-4a9b-8c0d-1e2f3a4b5c6d` with what the code does today." > "Leave an internal note on the decision saying the vendor quote expires on Friday." ## Related - [list_comments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-comments) — read the thread before you answer. - [link_artefacts](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/link-artefacts) — when the finding is a relation rather than a note. - [Comments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments) --- # convene_council Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council Convenes Heimdall's council on a brief import, a work item, a requirement, a business rule or a change request with the voices named, and answers with the sitting queued; spends the team's AI budget within the council's share. Convene Heimdall's council on one record and get the sitting back queued. Each seated voice (Odin for architecture, Thor for testing, Loki for security, Týr for rigour and compliance, Frigg for planning, Baldur for the people who use the product, Hel for production) reads the record and the project through its own lens, with the read-only tools the token's holder may use, and leaves findings. The voices research the ground before they read, answer one another over the rounds of the exchange and refine the plan Heimdall drafts from what they said; then Heimdall rules with the plan the council agreed. Poll the sitting with `get_council_session` until its status is `ready` or `failed`, then read the plan and the ranked findings. The record is named by `project_id`, `moment` (`intake_scaffold` on a brief import or on the project itself once Heimdall scaffolded it in conversation, `requirement_review` on a work item, a requirement or a business rule, `change_impact` on a change request against a baseline; the other moments open with their modules), `subject_type` (`import-batch` or `project`, `work-item`, `requirement`, `business-rule` or `change-request`) and `subject_id`. `voices` names the voices to summon by key; omit it for every voice the project seats on the moment, name one to ask one lens alone. Only voices the project seats on the moment sit; one sitting per record at a time. On the change impact moment the voices read the same brief Heimdall's impact analyst reads (the request, the impact written, the baseline and every record named with what links to it) and the verdict ranks what the change really touches, costs and risks. The call spends the team's monthly AI budget within the council's share of it, a call per voice per round plus Heimdall's plan and verdict, so read `list_council_sessions` before convening again on a record the council already sat on. It needs `ai:use`, a staff token and the project's assists on. Deciding a finding is a person's act: in the application, over REST, or through `decide_council_finding` on their word. - Requires ability: `ai:use` - Runs the same action as [`POST /v1/projects/{project}/council`](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/convene-the-council) - Fires events: [`council.convened`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-convened) ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `moment` | `intake_scaffold`, `requirement_review`, `change_impact` | yes | The moment the council sits on. | | `subject_type` | string | yes | The record's alias: import-batch or project for the intake scaffold; work-item, requirement or business-rule for the requirement review. | | `subject_id` | string | yes | UUID of the record the council reads. | | `voices` | array of `odin`, `thor`, `loki`, `tyr`, `frigg`, `baldur`, `hel` | no | The voices to summon, by key; omit for every voice the project seats on the moment. | ## Example call ```json { "name": "convene_council", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "moment": "intake_scaffold", "subject_type": "<subject_type>", "subject_id": "0de9d6ed-982e-4f00-a6c2-150ac4fe372e" } } ``` ## What it returns ```json { "data": { "id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "moment": "requirement_review", "subject_type": "work-item", "subject_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "subject_title": "Apply a voucher at checkout", "status": "queued", "voices": ["thor", "loki"], "round": 0, "phase": "research", "summary": null, "plan": null, "ranked": null, "disagreements": null, "finding_count": 0, "cost_micros": 0, "cost": "$0.0000", "failure_reason": null, "requested_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "requester_name": "Jane Doe", "started_at": null, "finished_at": null, "created_at": "2026-10-28T15:00:00+00:00", "turns": [ { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c03", "session_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "voice": "thor", "voice_name": "Thor", "lens": "Testing, verification and defects", "round": 0, "kind": "research", "status": "queued", "stance": null, "sources": null, "finding_count": 0, "failure_reason": null, "started_at": null, "finished_at": null }, { "id": "b8c9d0e1-f2a3-4b4c-8d5e-6f7a8b9c0d14", "session_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "voice": "loki", "voice_name": "Loki", "lens": "Security, abuse cases and data exposure", "round": 0, "kind": "research", "status": "queued", "stance": null, "sources": null, "finding_count": 0, "failure_reason": null, "started_at": null, "finished_at": null } ] } } ``` ## How it fails - `VALIDATION_FAILED` — the moment is not open (the response lists the open ones), no voice the project seats on the moment was named, the council is already sitting on the record, or the council's share of the month's budget is spent; nothing was written. - `RESOURCE_NOT_FOUND` — unknown project_id, or a subject_type and subject_id that do not name a visible record fitting the moment (the response lists the aliases that fit). - `FORBIDDEN` — the token is a client guest's, the holder's role does not carry ai:use, or the project's assists are off. - `AI_BUDGET_EXHAUSTED` — the team's monthly AI budget has no room; a team admin raises it under Settings → AI. - `TOKEN_MISSING_ABILITY` — token lacks ai:use. ## Example prompts > "Convene the council on CRM-142 before I pick it up, Thor and Loki only, and tell me what they find." > "Ask Odin alone about requirement `3a4b5c6d-7e8f-4a9b-8c0d-1e2f3a4b5c6d` and summarise his findings when the sitting is ready." > "Convene the council on the latest brief import of the Customer Portal and list the findings that draft a record." > "Sit the council on change request CRM-CR-2 and tell me which records it forgot." ## Related - [get_council_session](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-council-session) — poll the sitting and read the verdict and the findings. - [list_council_sessions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-council-sessions) — the sittings already held on the project. - [review_quality](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/review-quality) — Heimdall alone on one record, cheaper when one reading is enough. - [analyse_change_impact](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/analyse-change-impact) — Heimdall alone on a change request. - [Council API](https://alm-docs.labs.zuselab.dev/api/v1/reference/council) — the same sitting over REST, where a person accepts or dismisses a finding. --- # create_baseline Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-baseline Composes a draft baseline on a project from approved records, with its name, kind and words, and answers with the draft and its record rows; nothing is frozen until a person submits it and the project manager approves. Compose a baseline the people will freeze. `project_id` and a `name` unique within the project (3 to 120 characters) are required; `kind` is `scope`, `release`, `sprint` or `milestone` (default scope), `description` says what the baseline marks as HTML, and `items` names the records it holds as objects of `type` (`epic`, `work-item`, `requirement`, `business-rule` or `use-case`) and `id`. Only an approved record, one that carries a version, may join; a draft or a record in review is refused and nothing is written. The baseline is written as a draft with its rows in the order given, each noted at the record's approved version. Submitting it for the project manager's approval is a person's act, through `submit_for_review` on their word or in the application; the approval is what freezes it and locks the records. Runs the same action as **Create Baseline** in the application: a `baseline.created` event, a `baseline.updated` event for the records, and the first versions. - Requires ability: `baseline:create` - Runs the same action as [`POST /v1/projects/{project}/baselines`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/add-a-baseline) - Fires events: [`baseline.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `name` | string | yes | What the baseline is called, 3 to 120 characters, unique within the project. | | `kind` | `scope`, `release`, `sprint`, `milestone` | no | The kind of baseline. | | `description` | string | no | What the baseline marks, as HTML. | | `items` | array of any | no | The approved records it holds: objects with type (epic, work-item, requirement, business-rule or use-case) and id. | ## Example call ```json { "name": "create_baseline", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "v1.1 scope", "kind": "scope", "description": "<p>The second release, as agreed on the October call.</p>", "status": "draft", "frozen": false, "locks_items": false, "version": 0, "approved_at": null, "approved_revision_id": null, "frozen_at": null, "frozen_by": null, "signed_off_at": null, "items": [ { "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e", "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "artefact_type": "requirement", "artefact_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Password reset by email", "version": 2, "revision_id": null, "frozen_at": null, "locked": false, "unlocked_at": null, "unlocked_by_change_request_id": null, "position": 0 } ], "position": 1, "created_at": "2026-10-29T09:00:00+00:00", "updated_at": "2026-10-29T09:00:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — the name is missing, shorter than 3 or longer than 120 characters, or already used on the project; an unknown kind; an item whose type cannot be baselined or whose record is not approved; more than 500 items. - `RESOURCE_NOT_FOUND` — unknown project_id, or an item naming a record that is hidden, gone or another project's; nothing is written. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry baseline:create (and baseline:update for the items). - `TOKEN_MISSING_ABILITY` — token lacks baseline:create. ## Example prompts > "Compose a scope baseline called v1.1 scope from every approved requirement and rule of the Customer Portal, then tell me what to submit." > "Start a release baseline named Release 1.1 on project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` with the loyalty epic in it." > "Make an empty sprint baseline for sprint 3; I will add the stories after planning." ## Related - [update_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-baseline) — change a draft's words or records, or supersede a frozen one. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — put the draft to the project manager, on the person's word. - [list_requirements](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements) — find the approved records to hold, by status. - [Baselines API](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines) — the same write over REST. --- # create_epic Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-epic Adds an epic to a project as a draft, so Heimdall can split a brief into bodies of work and hang the stories under them. Add an epic: `project_id` and `title` are required; `summary` (HTML), `goal` (one sentence), `business_value` (HTML), `hypothesis`, `success_metrics` (HTML), `owner_id`, `priority` (`must`, `should`, `could` or `wont`; `should` by default), `starts_on`, `due_on` and `color` fill in the rest. The epic takes the next number of the project's tracker sequence, starts as a draft, writes its first version and is announced as `epic.created`. An epic reaches in review and approved through `submit_for_review` and the approvers, never by hand; the stories under it come from `create_story_draft` or `create_work_item` with the epic's id. A staff token with `epic:create` on the project is needed. - Requires ability: `epic:create` - Runs the same action as [`POST /v1/projects/{project}/epics`](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/add-an-epic) - Fires events: [`epic.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `title` | string | yes | What the epic is called, 3 to 200 characters. | | `summary` | string | no | What the epic covers, as HTML. | | `goal` | string | no | One sentence on what it achieves. | | `business_value` | string | no | Why the client wants it, as HTML. | | `hypothesis` | string | no | The bet it makes, if any. | | `success_metrics` | string | no | How the team will know it worked, as HTML. | | `owner_id` | string | no | UUID of the person who answers for it. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority; should by default. | | `starts_on` | string | no | When work on it starts, YYYY-MM-DD. | | `due_on` | string | no | When it is due, YYYY-MM-DD. | | `color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` | no | The badge colour the board and the roadmap paint it in. | ## Example call ```json { "name": "create_epic", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>" } } ``` ## What it returns ```json { "data": { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 1, "reference": "SCL-1", "title": "Order placement", "summary": "<p>A customer picks services and items and chooses pickup or drop-off.</p>", "goal": "A customer books a wash in under two minutes.", "business_value": null, "hypothesis": null, "success_metrics": null, "owner_id": null, "owner_name": null, "priority": "must", "status": "draft", "starts_on": null, "due_on": null, "color": null, "position": 1, "version": 0, "approved_at": null, "approved_revision_id": null, "created_at": "2026-10-28T09:55:00+00:00", "updated_at": "2026-10-28T09:55:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — title missing or outside 3 to 200 characters, a priority outside MoSCoW, a color outside the palette, a date that is not YYYY-MM-DD, or an owner_id who is not on the team. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry epic:create. - `TOKEN_MISSING_ABILITY` — token lacks epic:create. ## Example prompts > "Split the laundry brief into epics: the informational site, order placement, order tracking, and payments and appointments." > "Add an epic for the driver app, priority could." > "Create a must-have epic for online payments with a goal and success metrics." ## Related - [list_epics](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-epics) — the epics of the project today. - [update_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-epic) — change or move one later. - [create_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/create-work-item) — the stories under it. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — put it up for approval. - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) — the same door over REST. --- # create_raid_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-raid-item Adds any entry of the RAID log, an assumption, an issue or a dependency as well as a risk; the full form beside raise_risk. Add an item to a project's RAID log: `project_id` and `title` are required; `type` is `risk` by default or one of `assumption`, `issue`, `dependency`; `description` (HTML), `category`, `probability` and `impact` (1 to 5; probability on a risk, impact on a risk or an issue), `mitigation` and `contingency` (HTML, risks), `owner_id`, `due_on` (issues and dependencies), `review_on` (risks and assumptions), `depends_on` (what the project leans on, in words, dependencies) and `status` (`open` by default) fill in the rest; the columns a kind does not carry are dropped. The item takes the next number of the RAID sequence, names the person you act for as the one who raised it and is announced as `raid_item.created`. `raise_risk` remains the short road for a risk. A staff token with `raid:create` on the project is needed. - Requires ability: `raid:create` - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `title` | string | yes | What the item is called, 3 to 200 characters. | | `type` | `risk`, `assumption`, `issue`, `dependency` | no | Risk, assumption, issue or dependency; risk by default. | | `description` | string | no | What it is, as HTML. | | `category` | `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality`, `other` | no | The area of the project it touches; other by default. | | `probability` | integer | no | How likely, 1 to 5; risks only. | | `impact` | integer | no | How bad, 1 to 5; risks and issues. | | `mitigation` | string | no | What makes it less likely or less bad, as HTML; risks only. | | `contingency` | string | no | What happens if it occurs, as HTML; risks only. | | `owner_id` | string | no | UUID of the person who answers for it. | | `due_on` | string | no | When it must be resolved, YYYY-MM-DD; issues and dependencies. | | `review_on` | string | no | When it is next reviewed, YYYY-MM-DD; risks and assumptions. | | `depends_on` | string | no | What the project depends on, in words; dependencies only. | ## Example call ```json { "name": "create_raid_item", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>" } } ``` ## What it returns ```json { "data": { "id": "c9d0e1f2-a3b4-4c5d-8e6f-7a8b9c0d1e2f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 3, "reference": "SCL-RSK-3", "type": "issue", "title": "The gateway sandbox is not approved yet", "description": "<p>The payment provider has not issued sandbox keys; the checkout cannot be tested end to end.</p>", "category": "external", "probability": null, "impact": 4, "score": null, "mitigation": null, "contingency": null, "owner_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "owner_name": "Pat Manager", "raised_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "raised_by_name": "Bea Analyst", "due_on": "2026-11-15", "review_on": null, "depends_on": null, "status": "open", "position": 3, "created_at": "2026-10-28T11:00:00+00:00", "updated_at": "2026-10-28T11:00:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — title missing or outside 3 to 200 characters, a type, category or status outside its set, a rating outside 1 to 5, a date that is not YYYY-MM-DD, or an owner_id who is not on the team. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry raid:create. - `TOKEN_MISSING_ABILITY` — token lacks raid:create. ## Example prompts > "Log an issue: the payment sandbox is not approved, impact four, due mid-November, Pat owns it." > "Record the assumption that the shop keeps one price list for all branches." > "Add a dependency on the courier's API for live tracking." ## Related - [raise_risk](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-risk) — the short road for a risk. - [list_raid_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-raid-items) — the log today. - [update_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-raid-item) — change or move an item. - [RAID log API](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log) — the same door over REST. --- # create_requirement_draft Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-requirement-draft Add a functional or non-functional requirement to a project as a draft a person reviews, with its statement, source, priority, measure and verification method. Add a requirement to a project: a title, the kind (`functional` by default, or `non_functional` with the `category` the quality belongs to and a `measure` with the metric and the target), the statement in one testable sentence, the rationale, where it came from in words, a MoSCoW priority and how it will be shown to be met. The requirement takes the next number of its kind's sequence (`CRM-FR-12`, `CRM-NFR-3`), starts as a draft and writes its first version; `requirement.created` goes out. A bounded write: it adds one draft and nothing else, so the person reviews it on the Requirements page before anyone builds against it. Write the statement in the client's words and in the shall form, one behaviour per requirement, and give a non-functional requirement a number it can be measured against; a quality without a target cannot be tested. A non-functional requirement without a category is refused. - Requires ability: `requirement:create` - Runs the same action as [`POST /v1/projects/{project}/requirements`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/add-a-requirement) - Fires events: [`requirement.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-created) ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project the requirement belongs to. | | `title` | string | yes | What the requirement is called, 3 to 200 characters. | | `type` | `functional`, `non_functional` | no | The kind; functional by default. | | `category` | `performance`, `scalability`, `availability`, `reliability`, `security`, `privacy`, `usability`, `accessibility`, `compatibility`, `maintainability`, `portability`, `compliance`, `observability`, `localisation`, `other` | no | The quality a non-functional requirement is about; required for one. | | `statement` | string | no | What the system shall do or keep, as HTML, in one testable sentence. | | `rationale` | string | no | Why, as HTML. | | `source` | string | no | Where it came from, in words: a meeting, a document, a stakeholder. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority; should by default. | | `measure` | string | no | For a non-functional requirement, the metric and target that say when it is met, such as "p95 under 300 ms at 500 concurrent users". | | `verification_method` | `test`, `inspection`, `analysis`, `demonstration` | no | How it will be shown to be met; test by default. | ## Example call ```json { "name": "create_requirement_draft", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>" } } ``` ## What it returns ```json { "data": { "id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "type": "non_functional", "number": 4, "reference": "CRM-NFR-4", "category": "accessibility", "title": "Every screen meets WCAG 2.2 AA", "statement": "<p>The system shall meet WCAG 2.2 level AA on every screen a member can reach.</p>", "rationale": null, "source_stakeholder_id": null, "source_stakeholder_name": null, "source": "Public sector procurement rules", "priority": "must", "measure": "Zero level A or AA failures in an automated audit of every route, plus a manual keyboard pass", "verification_method": "inspection", "status": "draft", "owner_id": null, "owner_name": null, "position": 5, "version": 0, "approved_at": null, "approved_revision_id": null, "created_at": "2026-11-03T10:00:00+00:00", "updated_at": "2026-11-03T10:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — the title is missing or too long, a non-functional requirement has no category, or type, category, priority or verification_method is not one of its values. - `TOKEN_MISSING_ABILITY` — token lacks requirement:create. - `FORBIDDEN` — the token holder's role on the project does not carry requirement:create. ## Example prompts > "Draft the accessibility requirement for the portal: WCAG 2.2 AA on every screen, verified by inspection." > "Add a functional requirement to project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`: members can download a receipt as a PDF, must have." > "Turn the performance target from the workshop notes into a non-functional requirement with a measure." ## Related - [list_requirements](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements) — see what is there before you add one. - [get_requirement](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-requirement) — read the draft back. - [list_stakeholders](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-stakeholders) — who asked for it. - [Requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements) --- # create_story_draft Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-story-draft Add a story to a project's backlog with its sentence, its epic, its priority and its acceptance criteria, as a draft a person reviews. Add a story to a project's backlog: a title, the sentence in the persona's words (`as_a`, `i_want`, `so_that`), a description, the epic it hangs under, a priority and the acceptance criteria rows, each with its sentence, its kind and an optional Given, When and Then. The story takes the next number of the project's tracker sequence, starts in the backlog, names the person you act as its reporter and writes its first version with the criteria inside it; `work_item.created` goes out. A bounded write: it creates one story and nothing else, so the person reviews it on the Backlog page before anyone builds it. Write the criteria as a tester would tick them, a functional row for the happy path, a negative row for what must be refused and an edge row for the boundary, and keep the title short. The epic must be an epic of the same project. - Requires ability: `work-item:create` - Runs the same action as [`POST /v1/projects/{project}/work-items`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/add-a-work-item) - Fires events: [`work_item.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-created) ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project the story belongs to. | | `title` | string | yes | What the story is called, 3 to 200 characters. | | `as_a` | string | no | The persona, as in "As a returning shopper". | | `i_want` | string | no | What they want, as in "I want to reorder my last basket". | | `so_that` | string | no | Why, as in "so that I do not pick every item again". | | `description` | string | no | What the story is, as HTML. | | `epic_id` | string | no | UUID of the epic it hangs under, an epic of the same project. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | How urgent it is; medium by default. | | `acceptance_criteria` | array of any | no | The criteria that say when it is done: objects with title (required), given, when, then and kind (functional, negative, edge or non_functional). | ## Example call ```json { "name": "create_story_draft", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>" } } ``` ## What it returns ```json { "data": { "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 143, "reference": "CRM-143", "type": "story", "parent_id": null, "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Tier downgrade warning", "description": null, "status": "backlog", "resolution": null, "priority": "high", "assignee_id": null, "assignee_name": null, "reporter_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "reporter_name": "Nimal Fernando", "reviewer_id": null, "qa_owner_id": null, "story_points": null, "estimate_minutes": null, "remaining_minutes": null, "due_on": null, "started_at": null, "completed_at": null, "persona_id": null, "as_a": "platinum member", "i_want": "to be warned before I drop a tier", "so_that": "I can keep my perks", "business_value_points": null, "severity": null, "environment_id": null, "component_id": null, "steps_to_reproduce": null, "expected_result": null, "actual_result": null, "reproducible": null, "timebox_minutes": null, "outcome": null, "component_ids": [], "acceptance_criteria": [ { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c03", "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "position": 0, "title": "A member within reach of the threshold is warned thirty days ahead", "given": null, "when": null, "then": null, "kind": "functional", "verified_at": null, "verified_by": null }, { "id": "b8c9d0e1-f2a3-4b4c-8d5e-6f7a8b9c0d14", "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "position": 1, "title": "A member already below the threshold gets no warning", "given": null, "when": null, "then": null, "kind": "negative", "verified_at": null, "verified_by": null } ], "position": 4, "version": 0, "approved_at": null, "created_at": "2026-11-03T10:00:00+00:00", "updated_at": "2026-11-03T10:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — the title is missing or too long, a sentence part or a criterion is too long, a criterion lacks its sentence, priority or a criterion kind is not one of its values, or the epic is not an epic of the project. - `TOKEN_MISSING_ABILITY` — token lacks work-item:create. - `FORBIDDEN` — the token holder's role on the project does not carry work-item:create. ## Example prompts > "Draft a story under the loyalty tiers epic: a platinum member wants to be warned before they drop a tier, with a negative criterion." > "Add a story to project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` for reordering the last basket, with three acceptance criteria." > "Turn the third step of the checkout use case into a story in the backlog." ## Related - [list_epics](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-epics) — find the epic to hang the story under. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — read the story back. - [move_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/move-work-item) — move it to ready once a person has reviewed it. - [Work Items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) --- # create_use_case Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-use-case Adds a use case as a draft, with its main, alternate and exception flows, so how a persona reaches a goal is written step by step. Add a use case: `project_id` and `title` are required; `actor_persona_id` names the persona who acts, `secondary_actors` the others in words, `goal`, `trigger`, `preconditions` and `postconditions` (HTML), `frequency` and `priority` (`should` by default) fill in the record, and `steps` lays down the flows in one go, each row with `actor_action` (required), `system_response`, `flow` (`main` by default, or `alternate`, `exception`), `flow_label` such as "3a. Card declined" and `branches_from`, the main-flow step number an alternate or exception branches from. The use case takes the next number of its sequence, starts as a draft, writes its first version and is announced as `use_case.created`. A use case reaches in review and approved through `submit_for_review` and the approvers, never by hand. A staff token with `use-case:create` on the project is needed. - Requires ability: `use-case:create` - Runs the same action as [`POST /v1/projects/{project}/use-cases`](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/add-a-use-case) - Fires events: [`use_case.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `title` | string | yes | What the use case is called, 3 to 200 characters. | | `actor_persona_id` | string | no | UUID of the persona who acts. | | `secondary_actors` | array of string | no | The other actors in the room, in words; the list replaces the current one. | | `goal` | string | no | What the actor is trying to achieve. | | `trigger` | string | no | What starts it. | | `preconditions` | string | no | What must be true before, as HTML. | | `postconditions` | string | no | What is true after, as HTML. | | `frequency` | string | no | How often it happens. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority; should by default. | | `steps` | array of any | no | The steps replacing the list, in order: objects with actor_action (required), system_response, flow (main, alternate, exception), flow_label and branches_from; a row with an id keeps that step. | ## Example call ```json { "name": "create_use_case", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>" } } ``` ## What it returns ```json { "data": { "id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 2, "reference": "SCL-UC-2", "title": "Book a pickup", "actor_persona_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c", "actor_persona_name": "Busy parent", "secondary_actors": ["Driver"], "goal": "Have the laundry collected from home", "trigger": "The customer has an order ready to hand over", "preconditions": "<p>The order is paid.</p>", "postconditions": "<p>A pickup slot is held and the driver's route includes the address.</p>", "frequency": "Daily", "priority": "must", "status": "draft", "steps": [ { "id": "f6a7b8c9-d0e1-4f2a-8b3c-4d5e6f7a8b9c", "use_case_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "position": 1, "flow": "main", "flow_label": null, "actor_action": "Chooses a pickup slot", "system_response": "Shows the slots with capacity", "branches_from": null }, { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c0d", "use_case_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "position": 2, "flow": "main", "flow_label": null, "actor_action": "Confirms the booking", "system_response": "Sends the confirmation", "branches_from": null }, { "id": "b8c9d0e1-f2a3-4b4c-8d5e-6f7a8b9c0d1e", "use_case_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "position": 3, "flow": "alternate", "flow_label": "1a. Slot taken", "actor_action": "Picks a slot that just filled", "system_response": "Offers the next three slots", "branches_from": 1 } ], "position": 2, "version": 1, "approved_at": null, "approved_revision_id": null, "created_at": "2026-10-28T10:50:00+00:00", "updated_at": "2026-10-28T10:50:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — title missing or outside 3 to 200 characters, a priority outside MoSCoW, a persona that is not the project's, more steps than allowed, a step without an actor action, a flow outside the three, or a branch from a step that does not exist. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry use-case:create. - `TOKEN_MISSING_ABILITY` — token lacks use-case:create. ## Example prompts > "Write the use case for booking a pickup as the busy parent, with the slot-taken alternate flow." > "Draft use cases for placing an order, tracking it and collecting it." > "Add a use case for the shop manager marking an order ready, including the card-declined exception." ## Related - [list_use_cases](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-use-cases) — the use cases of the project today. - [update_use_case](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-use-case) — change the fields or the steps later. - [create_story_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-story-draft) — the stories a flow becomes. - [Use cases API](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases) — the same doors over REST. --- # decide_council_finding Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-council-finding Decides one finding of a council sitting on the person's word, after they confirm; accepting takes it up, dismissing sets it aside for good. Decide a council finding: `finding_id` and `verdict` (`accept` or `dismiss`) are required. Accepting a finding that drafts a record writes it as a draft through the ordinary action, stamped as the council's and accepted by the person; accepting one that only names a gap keeps it on the review queue for the project to read; dismissing remembers the idea so no voice raises it again. The decision goes through the same actions the Council tab runs. The council advises and a person decides, so the tool is destructive in the MCP sense: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Never decide a finding the person did not ask you to. A staff token with `import:update` on the project is needed. - Requires ability: `import:update` - Runs the same action as [`POST /v1/council-findings/{finding}/accept`](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/accept-a-council-finding) - Runs the same action as [`POST /v1/council-findings/{finding}/dismiss`](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/dismiss-a-council-finding) - Fires events: [`council.finding_accepted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-accepted), [`council.finding_dismissed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-dismissed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `finding_id` | string | yes | UUID of the council finding. | | `verdict` | `accept`, `dismiss` | yes | accept to take the finding up, dismiss to set it aside for good. | ## Example call ```json { "name": "decide_council_finding", "arguments": { "finding_id": "83d88bb6-dadb-4000-adef-f20ff9c0581a", "verdict": "accept" } } ``` ## What it returns ```json { "data": { "id": "f8a9b0c1-d2e3-4f4a-8b5c-6d7e8f9a0b1c", "session_id": "a9b0c1d2-e3f4-4a5b-8c6d-7e8f9a0b1c2d", "turn_id": "b0c1d2e3-f4a5-4b6c-8d7e-8f9a0b1c2d3e", "voice": "loki", "voice_name": "Loki", "round": 1, "position": 1, "title": "Keep order references out of the public tracking URL", "claim": "A guessable order reference in the URL lets anyone read another customer's order status.", "evidence": "Story SCL-8: 'the status page shows the current stage' with no mention of who may open it.", "suggestion": "Issue a signed tracking token per order and put the reference behind it.", "severity": "high", "confidence": 0.86, "kind": "non_functional_requirement", "payload": { "title": "Tracking pages are not enumerable", "category": "security" }, "status": "accepted", "suggestion_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "decided_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "decider_name": "Bea Analyst", "decided_at": "2026-10-28T12:30:00+00:00", "created_at": "2026-10-28T12:10:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a verdict other than accept or dismiss, a finding already decided, or a sitting that is not ready. - `RESOURCE_NOT_FOUND` — unknown finding_id, or a finding outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry import:update. - `TOKEN_MISSING_ABILITY` — token lacks import:update. ## Example prompts > "Accept Loki's finding about the tracking URL." > "Dismiss Frigg's point about a second sprint; we agreed one." > "Take up the first two findings and dismiss the rest." ## Related - [get_council_session](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-council-session) — the sitting with its findings and their ids. - [convene_council](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — a new sitting. - [decide_suggestion](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-suggestion) — the same decision on a review-queue row. - [Council API](https://alm-docs.labs.zuselab.dev/api/v1/reference/council) — the accept and dismiss doors over REST. --- # decide_suggestion Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-suggestion Decides one row of the review queue on the person's word, after they confirm; accepting writes the record, rejecting settles the row. Decide a suggestion of a brief import or a research sweep: `suggestion_id` and `verdict` (`accept` or `reject`) are required; `payload`, an object of the record's fields, replaces the drafted fields on an accept so a person's edits are written rather than the draft. Accepting a suggestion that drafts a record writes it as a draft through the ordinary action, stamped as Heimdall's and accepted by the person; a suggestion that drafts nothing, such as an improvement or a research finding, is kept on the queue as accepted for the project to read. Rejecting settles the row as rejected, or as dismissed for an idea that drafts no record, and the idea is not proposed again. The review queue exists so that a person decides, so the tool is destructive in the MCP sense: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Never decide a suggestion the person did not ask you to. A staff token with `import:update` on the project is needed. - Requires ability: `import:update` - Runs the same action as [`POST /v1/suggestions/{suggestion}/accept`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/accept-a-suggestion) - Runs the same action as [`POST /v1/suggestions/{suggestion}/reject`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/reject-a-suggestion) - Fires events: [`import.suggestion_accepted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-accepted), [`import.suggestion_rejected`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-rejected) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `suggestion_id` | string | yes | UUID of the suggestion. | | `verdict` | `accept`, `reject` | yes | accept to write the record, reject to settle the row without one. | | `payload` | object | no | On an accept, the record's fields as the person wants them written, replacing the drafted fields; omit to accept the draft as it is. | ## Example call ```json { "name": "decide_suggestion", "arguments": { "suggestion_id": "e66dc5ab-3610-4000-a85a-bc12419501bd", "verdict": "accept" } } ``` ## What it returns ```json { "data": { "id": "b4c5d6e7-f8a9-4b0c-8d1e-2f3a4b5c6d7e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "batch_id": "f2a3b4c5-d6e7-4f8a-8b9c-0d1e2f3a4b5c", "kind": "epic", "position": 1, "parent_id": null, "title": "Order placement", "payload": { "title": "Order placement", "goal": "A customer books a wash in under two minutes." }, "confidence": 0.92, "rationale": "The brief names ordering as the first thing the site must do.", "source_quote": "customers place laundry orders online", "source_span": { "start": 58, "end": 95 }, "voice": null, "status": "accepted", "duplicate_type": null, "duplicate_id": null, "created_type": "epic", "created_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "decided_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "decider_name": "Bea Analyst", "decided_at": "2026-10-28T11:40:00+00:00", "created_at": "2026-10-28T11:21:40+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a verdict other than accept or reject, a payload that is not an object or has more than 40 keys, a suggestion already decided, or a batch no longer ready. - `RESOURCE_NOT_FOUND` — unknown suggestion_id, or a suggestion outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry import:update. - `TOKEN_MISSING_ABILITY` — token lacks import:update. ## Example prompts > "Accept the four epics the import drafted." > "Accept the refund rule but call it 'Refunds within seven days'." > "Reject the suggestion about a loyalty scheme; the client does not want one." ## Related - [get_import_batch](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-import-batch) — the rows with their ids and payloads. - [decide_council_finding](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-council-finding) — the same decision on a council finding. - [Imports API](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports) — the accept, reject and merge doors over REST. --- # delete_baseline Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-baseline Deletes a draft baseline with its record rows and versions; a baseline past its draft is the record and is superseded rather than deleted. Remove a baseline that was never frozen. The draft, its record rows and every version of it are deleted and its name is free again; the records it listed are untouched, since a draft never locked them. A baseline that is in review, approved, awaiting the client's signature, signed or superseded cannot be deleted: it is the record the project was held to, and the answer says to supersede it instead. Marked destructive, so a client confirms before running it. Runs the same action as **Delete Baseline** in the application and emits `baseline.deleted`. - Requires ability: `baseline:delete` - Runs the same action as [`DELETE /v1/baselines/{baseline}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/delete-a-baseline) - Fires events: [`baseline.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `baseline_id` | string | yes | UUID of the draft baseline to delete. | ## Example call ```json { "name": "delete_baseline", "arguments": { "baseline_id": "a7a5d36a-225a-4800-a992-aad35d1bd7c6" } } ``` ## What it returns ```json { "data": { "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "deleted": true } } ``` ## How it fails - `VALIDATION_FAILED` — the baseline is past its draft; nothing is deleted. - `RESOURCE_NOT_FOUND` — unknown baseline_id, or a baseline outside the token's scope. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry baseline:delete. - `TOKEN_MISSING_ABILITY` — token lacks baseline:delete. ## Example prompts > "Delete the draft baseline Release 1.1; we are cutting the release from the sprint baseline instead." > "Remove baseline `6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d`." ## Related - [update_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-baseline) — supersede a frozen baseline instead. - [list_baselines](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-baselines) — find the draft by name or status. - [Baselines API](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines) — the same delete over REST. --- # delete_business_rule Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-business-rule Deletes a business rule after the person confirms; deprecating keeps it on the record instead. Delete a business rule by `business_rule_id`. Its reference is never reused and the links to it go. The removal goes through the same action the Business Rules page runs and `business_rule.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer moving a rule to `deprecated` with `update_business_rule`, so the record keeps what the business once decided. A staff token with `business-rule:delete` on the project is needed. - Requires ability: `business-rule:delete` - Runs the same action as [`DELETE /v1/business-rules/{rule}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/delete-a-business-rule) - Fires events: [`business_rule.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `business_rule_id` | string | yes | UUID of the business rule to delete. | ## Example call ```json { "name": "delete_business_rule", "arguments": { "business_rule_id": "25b88aea-9286-4600-a447-00cad0062444" } } ``` ## What it returns ```json { "data": { "business_rule_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown business_rule_id, or a rule outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry business-rule:delete. - `TOKEN_MISSING_ABILITY` — token lacks business-rule:delete. ## Example prompts > "Delete the rule I proposed twice." > "Remove SCL-BR-9; it duplicates SCL-BR-4." ## Related - [update_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-business-rule) — deprecate it instead. - [list_business_rules](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-business-rules) — what is left. - [Business rules API](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules) — the same door over REST. --- # delete_change_request Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-change-request Deletes a change request that was never agreed, with its links and versions; a request past its gate is closed rather than deleted, and its number is never handed out again. Remove a change request that is still `submitted`, `under_analysis` or `rejected`. The request, its links to the records it touched and every version of it are deleted; the records themselves are untouched, and the number stays spent. A request that is awaiting approval, approved or anywhere in the work after it cannot be deleted: withdraw the approval first, or close the request instead. Marked destructive, so a client confirms before running it. Runs the same action as **Delete Change Request** in the application and emits `change_request.deleted`. - Requires ability: `change-request:delete` - Runs the same action as [`DELETE /v1/change-requests/{changeRequest}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/delete-a-change-request) - Fires events: [`change_request.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `change_request_id` | string | yes | UUID of the change request to delete. | ## Example call ```json { "name": "delete_change_request", "arguments": { "change_request_id": "d2589e11-c5a2-4000-ac6f-e7a8e29873a7" } } ``` ## What it returns ```json { "data": { "change_request_id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "deleted": true } } ``` ## How it fails - `VALIDATION_FAILED` — the request is awaiting approval, approved, implementing, implemented, verified or closed; nothing is deleted. - `RESOURCE_NOT_FOUND` — unknown change_request_id, or a request outside the token's scope. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry change-request:delete. - `TOKEN_MISSING_ABILITY` — token lacks change-request:delete. ## Example prompts > "Delete CRM-CR-3; the client withdrew it before anyone read it." > "Remove change request `c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f`." ## Related - [update_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-change-request) — close a request instead. - [list_change_requests](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-change-requests) — find the request by reference or status. - [Change Requests API](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests) — the same delete over REST. --- # delete_decision Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-decision Deletes a decision after the person confirms; rejecting or deprecating keeps it on the record instead. Delete a decision by `decision_id`. Its reference is never reused and a decision that superseded it loses the link. The removal goes through the same action the Decisions page runs and `decision.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer moving a decision to `rejected` or `deprecated` with `update_decision`, so the record keeps why the team chose otherwise. A staff token with `decision:delete` on the project is needed. - Requires ability: `decision:delete` - Runs the same action as [`DELETE /v1/decisions/{decision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/delete-a-decision) - Fires events: [`decision.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `decision_id` | string | yes | UUID of the decision to delete. | ## Example call ```json { "name": "delete_decision", "arguments": { "decision_id": "bdee5679-3aa7-4c00-a3d9-2fc0e79341ff" } } ``` ## What it returns ```json { "data": { "decision_id": "d0e1f2a3-b4c5-4d6e-8f7a-8b9c0d1e2f3a", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown decision_id, or a decision outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry decision:delete. - `TOKEN_MISSING_ABILITY` — token lacks decision:delete. ## Example prompts > "Delete the decision I recorded twice." > "Remove SCL-DEC-5; it was never a decision, only a note." ## Related - [update_decision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-decision) — reject or deprecate it instead. - [list_decisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-decisions) — what is left. - [Decisions API](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions) — the same door over REST. --- # delete_epic Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-epic Deletes an epic after the person confirms; the work items under it stay without an epic. Delete an epic by `epic_id`. Its reference is never reused and the work items under it stay, without an epic. The removal goes through the same action the Epics page runs and `epic.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer moving an epic to `cancelled` with `update_epic`, so the record keeps what was once in scope. A staff token with `epic:delete` on the project is needed. - Requires ability: `epic:delete` - Runs the same action as [`DELETE /v1/epics/{epic}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/delete-an-epic) - Fires events: [`epic.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `epic_id` | string | yes | UUID of the epic to delete. | ## Example call ```json { "name": "delete_epic", "arguments": { "epic_id": "a8ab1070-211b-4c00-a69c-69c9d0e5a810" } } ``` ## What it returns ```json { "data": { "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown epic_id, or an epic outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry epic:delete. - `TOKEN_MISSING_ABILITY` — token lacks epic:delete. ## Example prompts > "Delete the epic I created twice." > "Remove the empty loyalty epic." ## Related - [update_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-epic) — cancel it instead. - [list_epics](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-epics) — what is left. - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) — the same door over REST. --- # delete_raid_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-raid-item Deletes a RAID item after the person confirms; closing it keeps it on the log instead. Delete a RAID item by `raid_item_id`. Its reference is never reused. The removal goes through the same action the RAID Log page runs and `raid_item.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer closing an item with `update_raid_item`, so the log keeps what the team watched. A staff token with `raid:delete` on the project is needed. - Requires ability: `raid:delete` - Runs the same action as [`DELETE /v1/raid-items/{raidItem}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/delete-a-raid-item) - Fires events: [`raid_item.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `raid_item_id` | string | yes | UUID of the RAID item to delete. | ## Example call ```json { "name": "delete_raid_item", "arguments": { "raid_item_id": "3b518d77-65d1-4e00-a5a9-90b3213d4d69" } } ``` ## What it returns ```json { "data": { "raid_item_id": "c9d0e1f2-a3b4-4c5d-8e6f-7a8b9c0d1e2f", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown raid_item_id, or an item outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry raid:delete. - `TOKEN_MISSING_ABILITY` — token lacks raid:delete. ## Example prompts > "Delete the risk I logged on the wrong project." > "Remove the duplicate courier dependency." ## Related - [update_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-raid-item) — close it instead. - [list_raid_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-raid-items) — what is left. - [RAID log API](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log) — the same door over REST. --- # delete_requirement Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-requirement Deletes a requirement after the person confirms; deprecating keeps it on the record instead. Delete a requirement by `requirement_id`. Its reference is never reused and the links to it go. The removal goes through the same action the Requirements page runs and `requirement.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer moving a requirement to `deprecated` with `update_requirement`, so the record keeps what was once asked. A staff token with `requirement:delete` on the project is needed. - Requires ability: `requirement:delete` - Runs the same action as [`DELETE /v1/requirements/{requirement}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/delete-a-requirement) - Fires events: [`requirement.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `requirement_id` | string | yes | UUID of the requirement to delete. | ## Example call ```json { "name": "delete_requirement", "arguments": { "requirement_id": "b0d1c030-4f2c-4000-aee6-b98971adea50" } } ``` ## What it returns ```json { "data": { "requirement_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown requirement_id, or a requirement outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry requirement:delete. - `TOKEN_MISSING_ABILITY` — token lacks requirement:delete. ## Example prompts > "Delete the duplicate requirement SCL-FR-19." > "Remove the NFR I drafted twice." ## Related - [update_requirement](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-requirement) — deprecate it instead. - [list_requirements](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements) — what is left. - [Requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements) — the same door over REST. --- # delete_use_case Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-use-case Deletes a use case and its steps after the person confirms; deprecating keeps it on the record instead. Delete a use case by `use_case_id`, with its steps. Its reference is never reused. The removal goes through the same action the Use Cases page runs and `use_case.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer moving a use case to `deprecated` with `update_use_case`, so the record keeps the flow. A staff token with `use-case:delete` on the project is needed. - Requires ability: `use-case:delete` - Runs the same action as [`DELETE /v1/use-cases/{useCase}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/delete-a-use-case) - Fires events: [`use_case.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `use_case_id` | string | yes | UUID of the use case to delete. | ## Example call ```json { "name": "delete_use_case", "arguments": { "use_case_id": "ca87f783-5324-4c00-a4b0-8e3a6df90725" } } ``` ## What it returns ```json { "data": { "use_case_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown use_case_id, or a use case outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry use-case:delete. - `TOKEN_MISSING_ABILITY` — token lacks use-case:delete. ## Example prompts > "Delete the use case I drafted twice." > "Remove SCL-UC-7; it is covered by SCL-UC-2." ## Related - [update_use_case](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-use-case) — deprecate it instead. - [list_use_cases](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-use-cases) — what is left. - [Use cases API](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases) — the same door over REST. --- # get_approval_request Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-approval-request Fetches one approval request by UUID with the record it is about, the approvers the policy named, the tally, the clocks and every verdict given, round by round. Read one approval request in full: the record under approval, the gate, where it stands, who asked and why, the approvers with how each was found (`owner`, `role`, `user` or `stakeholder`), the tally, the clocks and their rings, the version under review, and `decisions`, each with its round, the approver's name, the verdict, the reason and the signature hash. The same record `GET /v1/approvals/{approval}` answers. An unknown id, another tenant's request or a guest token the request does not name surfaces as `RESOURCE_NOT_FOUND`. Read it to explain why a record is still in review, who has answered and who has not; deciding stays with people. - Requires ability: `approval:view` - Runs the same action as [`GET /v1/approvals/{approval}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/get-an-approval-request) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `approval_id` | string | yes | UUID of the approval request to fetch. | ## Example call ```json { "name": "get_approval_request", "arguments": { "approval_id": "67beb6b6-7452-4800-a676-30f1febd951a" } } ``` ## What it returns ```json { "data": { "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "approvable_type": "requirement", "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Apply a voucher at checkout", "kind": "requirement_review", "status": "changes_requested", "rule": "any", "quorum": 1, "round": 1, "approvers": [ { "user_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "name": "Pat Manager", "via": "role", "stakeholder_id": null } ], "tally": { "approved": 0, "needed": 1, "total": 1 }, "requires_comment": true, "requested_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "requester_name": "Bea Analyst", "summary": "First cut of the voucher rules.", "revision_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "due_at": "2026-10-31T09:20:00+00:00", "remind_at": "2026-10-29T09:20:00+00:00", "auto_approve_at": null, "reminded_at": null, "escalated_at": null, "decided_at": null, "auto_approved": false, "decisions": [ { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a", "approval_request_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "round": 1, "approver_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "approver_name": "Pat Manager", "stakeholder_id": null, "verdict": "changes_requested", "comment": "Name the voucher kinds it applies to.", "signature_hash": "4f9c2a7e1b0d3c5f6a8e9b2d4c6f8a0b1c3d5e7f9a1b3c5d7e9f0a2b4c6d8e0f", "decided_at": "2026-10-28T11:05:00+00:00" } ], "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-10-28T11:05:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown approval_id, a request outside the token's scope or the person's visibility, or a guest token the request does not name. - `TOKEN_MISSING_ABILITY` — token lacks approval:view. ## Example prompts > "Why is CRM-FR-12 still in review? Who has answered?" > "Read approval `c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f` and tell me what Pat asked to change." > "Has the client signed the voucher requirement yet?" ## Related - [list_pending_approvals](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-pending-approvals) — the inbox. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — put a record up for approval. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — the version under review, by revision_id. - [Approvals API](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals) — the same request over REST. --- # get_baseline Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-baseline Fetches one baseline by id with the records it holds, each by family, key, title, the version pinned and whether it is still locked or was unlocked by a change request. Read one baseline in full. Beside the baseline's own fields (its kind, status, version, the clocks of the freeze and the signature) the answer carries `items`: every record the baseline holds, with `artefact_type` and `artefact_id`, the `reference` and `title` as they read when added, the `version` and `revision_id` pinned by the freeze, `frozen_at`, `locked`, and, once a change request released it, `unlocked_at` and `unlocked_by_change_request_id`. Use it to know which records a change request must name before it can unlock them, and which are already free. A client guest's token reads a frozen baseline the same way and never a draft. - Requires ability: `baseline:view` - Runs the same action as [`GET /v1/baselines/{baseline}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-baseline) - Runs the same action as [`GET /v1/baselines/{baseline}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-a-baselines-versions) - Runs the same action as [`GET /v1/baselines/{baseline}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/get-a-version-of-a-baseline) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `baseline_id` | string | yes | UUID of the baseline to fetch. | ## Example call ```json { "name": "get_baseline", "arguments": { "baseline_id": "a7a5d36a-225a-4800-a992-aad35d1bd7c6" } } ``` ## What it returns ```json { "data": { "id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "v1.0 scope", "kind": "scope", "description": "<p>Everything the client signed for the first release.</p>", "status": "approved", "frozen": true, "locks_items": true, "version": 1, "approved_at": "2026-10-27T15:40:00+00:00", "approved_revision_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "frozen_at": "2026-10-27T15:40:00+00:00", "frozen_by": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "signed_off_at": null, "items": [ { "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e", "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "artefact_type": "requirement", "artefact_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Password reset by email", "version": 2, "revision_id": "1f2a3b4c-5d6e-4f7a-8b9c-0d1e2f3a4b5c", "frozen_at": "2026-10-27T15:40:00+00:00", "locked": false, "unlocked_at": "2026-10-29T11:05:00+00:00", "unlocked_by_change_request_id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "position": 0 }, { "id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "artefact_type": "business-rule", "artefact_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "reference": "CRM-BR-7", "title": "One voucher per order", "version": 1, "revision_id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d", "frozen_at": "2026-10-27T15:40:00+00:00", "locked": true, "unlocked_at": null, "unlocked_by_change_request_id": null, "position": 1 } ], "position": 0, "created_at": "2026-10-27T09:00:00+00:00", "updated_at": "2026-10-29T11:05:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown baseline_id, a baseline outside the token's scope, or a draft read by a client guest's token. - `TOKEN_MISSING_ABILITY` — token lacks baseline:view. ## Example prompts > "What does the v1.0 scope hold, and which of its records are still locked?" > "Show me baseline `6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d` with its records." > "Was CRM-FR-12 unlocked, and by which change request?" ## Related - [list_baselines](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-baselines) — the project's baselines. - [raise_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-change-request) — the way a locked record is unlocked. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — the versions of the baseline itself, and of the records it pinned. - [Baselines API](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines) — the same record over REST. --- # get_business_rule Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-business-rule One business rule by id, with its reference, name, kind, statement, condition and action, exceptions, worked examples, authority, priority, dates in force, status and owner. Return one business rule: its reference, name, kind, the rule in plain words, when it applies and what follows, the exceptions, the worked examples in order, the stakeholder and the words its authority rests on, MoSCoW priority, the dates it is in force, status and owner. Read it when a story, a requirement or a conversation names a rule by reference and you need the exact decision the business made, with its examples, before you code the calculation or write the test. A rule of a project the token may not see, or a draft read through a guest token, is unknown rather than forbidden. - Requires ability: `business-rule:view` - Runs the same action as [`GET /v1/business-rules/{rule}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-business-rule) - Runs the same action as [`GET /v1/business-rules/{rule}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-versions) - Runs the same action as [`GET /v1/business-rules/{rule}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/get-a-version-of-a-business-rule) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `business_rule_id` | string | yes | UUID of the business rule to fetch. | ## Example call ```json { "name": "get_business_rule", "arguments": { "business_rule_id": "25b88aea-9286-4600-a447-00cad0062444" } } ``` ## What it returns ```json { "data": { "id": "d5e6f7a8-b9c0-4d1e-8f2a-3b4c5d6e7f80", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 7, "reference": "CRM-BR-7", "name": "Platinum tier threshold", "type": "inference", "statement": "<p>A member whose spend over the last twelve months reaches 5,000 is a platinum member.</p>", "condition": "When a member's rolling twelve-month spend reaches 5,000", "action": "then the member moves to the platinum tier on the first of the next month", "exceptions": "<p>Spend refunded within thirty days does not count.</p>", "examples": [ { "input": "Spend 4,999 on 30 November", "expected": "Stays gold" }, { "input": "Spend 5,000 on 30 November", "expected": "Platinum from 1 December" } ], "authority_stakeholder_id": "d6e7f8a9-b0c1-4d2e-9f3a-4b5c6d7e8f90", "authority_stakeholder_name": "Amal Jayasuriya", "authority": "Loyalty programme terms, section 4", "priority": "must", "effective_from": "2027-01-01", "effective_to": null, "status": "approved", "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "owner_name": "Nimal Fernando", "position": 6, "version": 0, "approved_at": "2026-11-05T15:00:00+00:00", "approved_revision_id": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-05T15:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown business_rule_id, a rule of a project outside the token's scope or the person's visibility, or a draft read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks business-rule:view. ## Example prompts > "What exactly does CRM-BR-7 say, and what are its worked examples?" > "Read rule `d5e6f7a8-b9c0-4d1e-8f2a-3b4c5d6e7f80` and tell me who may change it." > "Is the platinum threshold rule in force yet, and what are its exceptions?" ## Related - [list_business_rules](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-business-rules) - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — every version of the rule, examples included. - [Business Rules API](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules) --- # get_change_request Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-change-request Fetches one change request by id with the records it touches, its impact, Heimdall's draft, its baseline and the people behind it; a guest token reads it without the effort, the cost and the draft. Read one change request in full. Beside its own fields the answer carries `affected`: every record the request touches, with `artefact_type`, `artefact_id`, and the `reference` and `title` as they read now (null when the record is gone or hidden from the token's holder). Use it to know what an approval will unlock, to read the impact the analyst wrote and Heimdall's draft under `ai_impact_draft` with `analysed_at`, and to read the approver's words under `decision_note` on a rejected request. A client guest's token reads the requests they raised or asked for, without `impact_effort_hours`, `impact_cost` and `ai_impact_draft`. - Requires ability: `change-request:view` - Runs the same action as [`GET /v1/change-requests/{changeRequest}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-change-request) - Runs the same action as [`GET /v1/change-requests/{changeRequest}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-a-change-requests-versions) - Runs the same action as [`GET /v1/change-requests/{changeRequest}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/get-a-version-of-a-change-request) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `change_request_id` | string | yes | UUID of the change request to fetch. | ## Example call ```json { "name": "get_change_request", "arguments": { "change_request_id": "d2589e11-c5a2-4000-ac6f-e7a8e29873a7" } } ``` ## What it returns ```json { "data": { "id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 2, "reference": "CRM-CR-2", "title": "Accept vouchers on subscriptions too", "description": "<p>The client wants the voucher applied to the first renewal as well.</p>", "reason": "client_request", "requested_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "requester_name": "Bea Analyst", "stakeholder_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "stakeholder_name": "Gail of the Client", "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "baseline_name": "v1.0 scope", "affected": [ { "artefact_type": "requirement", "artefact_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Password reset by email" }, { "artefact_type": "work-item", "artefact_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "reference": "CRM-142", "title": "Apply a voucher at checkout" } ], "impact_scope": "<p>CRM-FR-12 gains a renewal clause; CRM-142 and its checkout tests change.</p>", "impact_schedule_days": 5, "impact_effort_hours": "24.00", "impact_cost": "1800.00", "impact_risk": "<p>The renewal job runs unattended; a voucher applied twice would refund twice.</p>", "impact_level": "medium", "ai_impact_draft": "<p>A small change to the renewal flow; the story that implements the requirement changes with it.</p><h4>Scope</h4><p>…</p>", "analysed_at": "2026-10-29T10:40:00+00:00", "priority": "high", "status": "approved", "decision_note": null, "verified_by": null, "verified_at": null, "version": 1, "approved_at": "2026-10-29T11:05:00+00:00", "approved_revision_id": "3b4c5d6e-7f8a-4b9c-8d0e-1f2a3b4c5d6e", "position": 1, "created_at": "2026-10-29T09:20:00+00:00", "updated_at": "2026-10-29T11:05:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown change_request_id, a request outside the token's scope, or one a client guest's token neither raised nor asked for. - `TOKEN_MISSING_ABILITY` — token lacks change-request:view. ## Example prompts > "What does CRM-CR-2 touch, and what did Heimdall say its impact is?" > "Read change request `c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f` and tell me why it was rejected." > "Is the voucher change request approved yet, and which records did it unlock?" ## Related - [list_change_requests](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-change-requests) — the project's requests. - [get_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-baseline) — the baseline the request changes, record by record. - [get_approval_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-approval-request) — the approval with its verdicts. - [Change Requests API](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests) — the same record over REST. --- # get_council_session Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-council-session One sitting of Heimdall's council with everything it holds, the verdict, the voices' turns and every finding with what a person decided; free to call. Return one sitting of Heimdall's council in full: the moment, the record it read, the voices, where it stands and the phase of its deliberation, Heimdall's verdict once written (the plan the council agreed, the summary, the findings ranked by id with a reason each, and where the voices disagreed), the voices' turns round by round with what each did (research with its sources, reading, replies, refinement) and their words, and every finding with its claim, the evidence quoted, the suggestion in the record's own words, the severity, the confidence, the record it drafted when it did (`kind` and `payload`) and what a person decided. Poll it after `convene_council` while the status is `queued`, `running` or `summarising`; once `ready`, read the ranked findings first, they are what the analyst reads. The sitting is named by `session_id`. Deciding a finding stays with people, in the application and over REST; an agent reads what the council found and says so. A guest token is refused. - Requires ability: `import:view` - Runs the same action as [`GET /v1/council/{session}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/retrieve-a-council-session) - Runs the same action as [`GET /v1/imports/{batch}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/get-an-import) - Runs the same action as [`GET /v1/imports/{batch}/suggestions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-an-imports-suggestions) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `session_id` | string | yes | UUID of the council session. | ## Example call ```json { "name": "get_council_session", "arguments": { "session_id": "077580e5-b95d-4b40-a66b-d06a41fcc073" } } ``` ## What it returns ```json { "data": { "id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "moment": "requirement_review", "subject_type": "work-item", "subject_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "subject_title": "Apply a voucher at checkout", "status": "ready", "voices": ["thor", "loki"], "round": 3, "phase": "verdict", "summary": "The story is sound; one criterion cannot be verified and the voucher code is personal data the record does not say how to keep.", "plan": "1. Name the limit on the voucher criterion.\n2. Add the masking rule, naming the logs and the analytics.", "ranked": [ { "finding_id": "f6a7b8c9-d0e1-4f2a-8b3c-4d5e6f7a8b92", "reason": "The rule applies to every voucher." }, { "finding_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "reason": "A tester is blocked until the limit is named." } ], "disagreements": [], "finding_count": 2, "cost_micros": 12500, "cost": "$0.0125", "failure_reason": null, "requested_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "requester_name": "Jane Doe", "started_at": "2026-10-28T15:00:05+00:00", "finished_at": "2026-10-28T15:03:00+00:00", "created_at": "2026-10-28T15:00:00+00:00", "turns": [ { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c03", "session_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "voice": "thor", "voice_name": "Thor", "lens": "Testing, verification and defects", "round": 1, "kind": "reading", "status": "ready", "stance": "One criterion cannot be verified as written.", "sources": null, "finding_count": 1, "failure_reason": null, "started_at": "2026-10-28T15:00:05+00:00", "finished_at": "2026-10-28T15:01:10+00:00" } ], "findings": [ { "id": "f6a7b8c9-d0e1-4f2a-8b3c-4d5e6f7a8b92", "session_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "turn_id": "b8c9d0e1-f2a3-4b4c-8d5e-6f7a8b9c0d14", "voice": "loki", "voice_name": "Loki", "round": 1, "position": 1, "title": "Keep voucher codes out of the logs", "claim": "A voucher code ties a purchase to a person; nothing says it is masked in logs and analytics.", "evidence": null, "suggestion": "Add a rule that voucher codes are masked before any log line.", "severity": "medium", "confidence": 0.7, "kind": "business_rule", "payload": { "name": "Keep voucher codes out of the logs", "type": "policy", "statement": "<p>Voucher codes are never written to logs or analytics in clear.</p>", "condition": "When a voucher is applied or refused", "action": "Then the code is masked before any log line or event is written", "exceptions": null, "examples": ["ABCD-1234 → ABCD-****"] }, "status": "pending", "suggestion_id": null, "decided_by": null, "decider_name": null, "decided_at": null, "created_at": "2026-10-28T15:02:00+00:00" } ] } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown session_id, or a sitting of a project outside the token's scope or the person's visibility; a client guest's token reads every sitting as not found. - `TOKEN_MISSING_ABILITY` — token lacks import:view. ## Example prompts > "Read the council's sitting `d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70` and tell me the three findings that matter most." > "Is the council done with CRM-142? If so, which findings did Thor raise?" > "Summarise the disagreements in the latest sitting on the Customer Portal." ## Related - [list_council_sessions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-council-sessions) — find the sitting to read. - [convene_council](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — convene a new sitting. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — the record a requirement review sat on. - [Council API](https://alm-docs.labs.zuselab.dev/api/v1/reference/council) — the same sitting over REST, where a person accepts or dismisses a finding. --- # get_decision Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-decision One decision by id, with its reference, kind, title, context, the options weighed, what was decided, consequences, status, the day decided, deciders and the decision it supersedes. Return one decision: its reference, kind, title, the context it was taken in, the options weighed in order with their pros and cons, what was decided, what follows, where it stands, the day it was decided, who decided and the earlier decision it replaces. Read it when a story, a context pack or a conversation names a decision by reference and you need the exact choice and its reasoning before you build on it. A decision of a project the token may not see, or any decision read through a guest token, is unknown rather than forbidden. - Requires ability: `decision:view` - Runs the same action as [`GET /v1/decisions/{decision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-decision) - Runs the same action as [`GET /v1/decisions/{decision}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-a-decisions-versions) - Runs the same action as [`GET /v1/decisions/{decision}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/get-a-version-of-a-decision) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `decision_id` | string | yes | UUID of the decision to fetch. | ## Example call ```json { "name": "get_decision", "arguments": { "decision_id": "bdee5679-3aa7-4c00-a3d9-2fc0e79341ff" } } ``` ## What it returns ```json { "data": { "id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 5, "reference": "CRM-DEC-5", "kind": "architecture", "title": "Keep the loyalty ledger in Postgres", "context": "<p>The ledger must be queried with the orders and audited.</p>", "options": [ { "title": "A ledger table in the main Postgres database", "pros": "One backup, one transaction, one place to audit", "cons": "Table growth needs partitioning within two years" }, { "title": "A dedicated event store", "pros": "Append-only by design", "cons": "A second system to run and to secure" } ], "decision": "<p>The ledger lives in Postgres, partitioned by month from the first release.</p>", "consequences": "<p>The reporting queries join the ledger directly.</p>", "status": "accepted", "decided_on": "2026-11-04", "deciders": ["c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f"], "supersedes_id": "b9c0d1e2-f3a4-4b5c-8d6e-7f8a9b0c1d24", "supersedes_reference": "CRM-DEC-2", "position": 4, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-04T15:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown decision_id, a decision of a project outside the token's scope or the person's visibility, or any decision read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks decision:view. ## Example prompts > "What exactly did CRM-DEC-5 decide, and what were the other options?" > "Read decision `a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13` and tell me what it replaced." > "Who decided to keep the ledger in Postgres, and when?" ## Related - [list_decisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-decisions) - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — every version of the decision, options included. - [Decisions API](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions) --- # get_epic Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-epic One epic by id, with its reference, goal, prose, owner, priority, status, dates and colour. Return one epic: its reference, title, goal, summary, business value, hypothesis, success metrics, owner, MoSCoW priority, status, dates and colour. Read it when a story, a commit or a conversation names an epic by reference and you need the goal and the scope it was agreed with, or to check where it stands before you move work under it. An epic of a project the token may not see, or an epic that is not agreed read through a guest token, is unknown rather than forbidden. - Requires ability: `epic:view` - Runs the same action as [`GET /v1/epics/{epic}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-an-epic) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `epic_id` | string | yes | UUID of the epic to fetch. | ## Example call ```json { "name": "get_epic", "arguments": { "epic_id": "a8ab1070-211b-4c00-a69c-69c9d0e5a810" } } ``` ## What it returns ```json { "data": { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 12, "reference": "CRM-12", "title": "Loyalty tiers", "summary": "<p>Members climb silver, gold and platinum tiers by annual spend.</p>", "goal": "Reward the top fifth of members so they keep buying from the client.", "business_value": "<p>Retention of the top tier is worth more than every acquisition campaign combined.</p>", "hypothesis": "A visible next tier raises repeat purchases within a quarter.", "success_metrics": "<p>Repeat purchase rate of gold members up ten points by the second quarter after launch.</p>", "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "owner_name": "Nimal Fernando", "priority": "must", "status": "approved", "starts_on": "2026-11-02", "due_on": "2027-03-31", "color": "amber", "position": 0, "version": 0, "approved_at": "2026-11-05T15:00:00+00:00", "approved_revision_id": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-05T15:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown epic_id, an epic of a project outside the token's scope or the person's visibility, or an epic that is not agreed read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks epic:view. ## Example prompts > "What is the goal of the loyalty tiers epic?" > "Read epic `a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d` and tell me what would count as success." > "Is CRM-12 approved yet, and who owns it?" ## Related - [list_epics](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-epics) - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — every version of the epic. - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) --- # get_import_batch Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-import-batch Fetches one brief import with its words and every suggestion Heimdall drafted from it, each with its decision so far. Read one brief import by its id, the `import_batch_id` argument: the batch with its status and counts, the brief's words, and `suggestions`, each with its kind, title, payload, confidence, rationale, the span of the brief it came from, its status (`pending`, `accepted`, `edited`, `merged`, `rejected` or `dismissed`), the record it created when accepted and the existing record it may duplicate. Poll it after `start_brief_import` until the status is `ready` or `failed`. Deciding a suggestion is the person's call, on the Brief Import page or through `decide_suggestion` on their word. A staff token with `import:view` is needed; an unknown or out-of-scope id is `RESOURCE_NOT_FOUND`. - Requires ability: `import:view` - Runs the same action as [`GET /v1/council/{session}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/retrieve-a-council-session) - Runs the same action as [`GET /v1/imports/{batch}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/get-an-import) - Runs the same action as [`GET /v1/imports/{batch}/suggestions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-an-imports-suggestions) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `import_batch_id` | string | yes | UUID of the brief import. | ## Example call ```json { "name": "get_import_batch", "arguments": { "import_batch_id": "f03525c5-e6ec-4800-ae02-5c7c68bab353" } } ``` ## What it returns ```json { "data": { "id": "f2a3b4c5-d6e7-4f8a-8b9c-0d1e2f3a4b5c", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "source": "pasted_text", "status": "ready", "title": "Laundry shop brief", "excerpt": "An informational website for a laundry shop with order placement, tracking, payment and appointment scheduling…", "text": "An informational website for a laundry shop with order placement, tracking, payment and appointment scheduling. Customers choose services…", "suggestion_count": 23, "pending_count": 21, "provider": "anthropic", "model": "claude-sonnet-5-5", "usage_id": "a3b4c5d6-e7f8-4a9b-8c0d-1e2f3a4b5c6d", "failure_reason": null, "created_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "creator_name": "Bea Analyst", "started_at": "2026-10-28T11:20:05+00:00", "finished_at": "2026-10-28T11:21:40+00:00", "created_at": "2026-10-28T11:20:00+00:00", "updated_at": "2026-10-28T11:21:40+00:00", "suggestions": [ { "id": "b4c5d6e7-f8a9-4b0c-8d1e-2f3a4b5c6d7e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "batch_id": "f2a3b4c5-d6e7-4f8a-8b9c-0d1e2f3a4b5c", "kind": "epic", "position": 1, "parent_id": null, "title": "Order placement", "payload": { "title": "Order placement", "goal": "A customer books a wash in under two minutes." }, "confidence": 0.92, "rationale": "The brief names ordering as the first thing the site must do.", "source_quote": "customers place laundry orders online", "source_span": { "start": 58, "end": 95 }, "voice": null, "status": "pending", "duplicate_type": null, "duplicate_id": null, "created_type": null, "created_id": null, "decided_by": null, "decider_name": null, "decided_at": null, "created_at": "2026-10-28T11:21:40+00:00" } ] } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown import_batch_id, or a batch outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry import:view. - `TOKEN_MISSING_ABILITY` — token lacks import:view. ## Example prompts > "Is the laundry brief import ready? What did it draft?" > "Show me the requirements Heimdall drafted from the brief and which ones are still pending." > "Which suggestions in the import duplicate something we already have?" ## Related - [list_import_batches](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-import-batches) — every reading of the project. - [decide_suggestion](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-suggestion) — a row decided on the person's word. - [convene_council](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — the council on the batch. - [Imports API](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports) — the same record over REST. --- # get_raid_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-raid-item One RAID item by id, with its reference, kind, title, description, area, probability, impact and score, mitigation and contingency, owner and raiser, dates, dependency and status. Return one entry of a project's RAID log: its reference, kind (risk, assumption, issue or dependency), title, description, the area it touches, the probability and the impact and the score they give, how a risk is mitigated and what happens if it occurs, who owns it and who raised it, when it is due or next reviewed, what a dependency depends on and where it stands. Read it when a story, a sprint or a conversation names an item by reference and you need what the team already decided about it. An item of a project the token may not see, or any item read through a guest token, is unknown rather than forbidden. - Requires ability: `raid:view` - Runs the same action as [`GET /v1/raid-items/{raidItem}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-raid-item) - Runs the same action as [`GET /v1/raid-items/{raidItem}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-a-raid-items-versions) - Runs the same action as [`GET /v1/raid-items/{raidItem}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/get-a-version-of-a-raid-item) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `raid_item_id` | string | yes | UUID of the RAID item to fetch. | ## Example call ```json { "name": "get_raid_item", "arguments": { "raid_item_id": "3b518d77-65d1-4e00-a5a9-90b3213d4d69" } } ``` ## What it returns ```json { "data": { "id": "f7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b02", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 4, "reference": "CRM-RSK-4", "type": "risk", "title": "Payment provider sandbox may lag behind production", "description": "<p>The provider ships new card flows to production weeks before its sandbox.</p>", "category": "external", "probability": 3, "impact": 4, "score": 12, "mitigation": "<p>Run the checkout smoke test against the provider's production account every Monday.</p>", "contingency": "<p>Pin the gateway to the last verified flow and raise an incident with the provider.</p>", "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "owner_name": "Nimal Fernando", "raised_by": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "raised_by_name": "Sithara Perera", "due_on": null, "review_on": "2026-11-16", "depends_on": null, "status": "monitoring", "position": 3, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-02T15:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown raid_item_id, an item of a project outside the token's scope or the person's visibility, or any item read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks raid:view. ## Example prompts > "What is the contingency for CRM-RSK-4?" > "Read item `f7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b02` and tell me who owns it and when it is next reviewed." > "Has the sandbox risk occurred, or is it still being watched?" ## Related - [list_raid_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-raid-items) - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — every version of the item. - [RAID Log API](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log) --- # get_requirement Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-requirement One requirement by id, with its reference, kind, category, statement, rationale, source, priority, measure, verification method, status and owner. Return one requirement: its reference, kind, the category and the measure of a non-functional one, title, statement, rationale, the stakeholder and the words it came from, MoSCoW priority, how it will be shown to be met, status and owner. Read it when a story, a test case or a conversation names a requirement by reference and you need the exact sentence the client agreed, or to check where it stands before you build against it. A requirement of a project the token may not see, or a draft read through a guest token, is unknown rather than forbidden. - Requires ability: `requirement:view` - Runs the same action as [`GET /v1/requirements/{requirement}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-requirement) - Runs the same action as [`GET /v1/requirements/{requirement}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-versions) - Runs the same action as [`GET /v1/requirements/{requirement}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/get-a-version-of-a-requirement) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `requirement_id` | string | yes | UUID of the requirement to fetch. | ## Example call ```json { "name": "get_requirement", "arguments": { "requirement_id": "b0d1c030-4f2c-4000-aee6-b98971adea50" } } ``` ## What it returns ```json { "data": { "id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "type": "functional", "number": 12, "reference": "CRM-FR-12", "category": null, "title": "Password reset by email", "statement": "<p>The system shall let a member reset their password from the sign-in page by email.</p>", "rationale": "<p>Support spends an hour a day resetting passwords by hand.</p>", "source_stakeholder_id": "d6e7f8a9-b0c1-4d2e-9f3a-4b5c6d7e8f90", "source_stakeholder_name": "Amal Jayasuriya", "source": "Discovery workshop, 14 October", "priority": "must", "measure": null, "verification_method": "demonstration", "status": "approved", "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "owner_name": "Nimal Fernando", "position": 0, "version": 0, "approved_at": "2026-11-05T15:00:00+00:00", "approved_revision_id": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-05T15:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown requirement_id, a requirement of a project outside the token's scope or the person's visibility, or a draft read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks requirement:view. ## Example prompts > "What exactly does CRM-FR-12 say, and how will it be verified?" > "Read requirement `c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f` and tell me who asked for it." > "Is the tier page performance requirement approved yet, and what is its target?" ## Related - [list_requirements](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements) - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — every version of the requirement. - [Requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements) --- # get_revision Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-revision One version of an artefact by id, with its snapshot, the diff from the version before, who wrote it and why. Return one version of a revisable artefact: the version number, the reason it was written, who wrote it and through what, the summary, the columns it changed, the diff from the version before and the full snapshot of the captured columns. Read it before you restore a version, so you and the person you act for know exactly what will come back, and to quote what a field said at a point in time. Needs `activity:read`. An unknown id, a version of an artefact the token may not read, or any version read through a guest token is unknown rather than forbidden. - Requires ability: `activity:read` - Runs the same action as [`GET /v1/epics/{epic}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/get-a-version-of-an-epic) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `revision_id` | string | yes | UUID of the version to fetch. | ## Example call ```json { "name": "get_revision", "arguments": { "revision_id": "e07da6e0-b4e0-4000-ae4a-df59c7fd1a20" } } ``` ## What it returns ```json { "data": { "id": "d6e7f8a9-b0c1-4d2e-9f3a-4b5c6d7e8f90", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "revisable_type": "epic", "revisable_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "version": 1, "reason": "created", "actor_kind": "human", "author_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "author_name": "Nimal Fernando", "ai_feature": null, "restored_from_id": null, "restored_from_version": null, "summary": "Created", "changed_fields": [], "diff": null, "snapshot": { "title": "Loyalty tiers", "summary": null, "goal": "Reward the top fifth of members so they keep buying from the client.", "business_value": null, "hypothesis": null, "success_metrics": null, "owner_id": null, "priority": "should", "status": "draft", "starts_on": null, "due_on": null, "color": null }, "created_at": "2026-10-28T09:20:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown revision_id, a version of an artefact outside the token's scope or the person's visibility, or any version read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks activity:read. ## Example prompts > "Show me version 1 of the loyalty tiers epic." > "Read revision `d6e7f8a9-b0c1-4d2e-9f3a-4b5c6d7e8f90` and tell me what the goal said then." > "Before you restore it, what exactly is in that version?" ## Related - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) - [restore_revision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/restore-revision) - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) --- # get_use_case Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-use-case One use case by id, with its reference, title, actor, other actors, goal, trigger, conditions, frequency, priority, status and its steps in order, each in the main flow or in an alternate or exception flow that leaves a main step. Return one use case with its steps: the reference, the title, the persona who acts and the other actors, the goal, the trigger, what must be true before and after, how often it happens, priority and status, and `steps` in order. Each step says what the actor does and what the system does in reply, which `flow` it belongs to (`main`, `alternate` or `exception`), and, for a branch, its `flow_label` (such as `2a`) and the main step it leaves in `branches_from`, numbered from one. Walk the main flow first and then each branch when you implement or test a use case, so the happy path and every way it goes differently are both covered. A use case of a project the token may not see, or a draft read through a guest token, is unknown rather than forbidden. - Requires ability: `use-case:view` - Runs the same action as [`GET /v1/use-cases/{useCase}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-use-case) - Runs the same action as [`GET /v1/use-cases/{useCase}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-a-use-cases-versions) - Runs the same action as [`GET /v1/use-cases/{useCase}/revisions/{revision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/get-a-version-of-a-use-case) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `use_case_id` | string | yes | UUID of the use case to fetch. | ## Example call ```json { "name": "get_use_case", "arguments": { "use_case_id": "ca87f783-5324-4c00-a4b0-8e3a6df90725" } } ``` ## What it returns ```json { "data": { "id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 4, "reference": "CRM-UC-4", "title": "Redeem a voucher at checkout", "actor_persona_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "actor_persona_name": "Nadeesha, the returning shopper", "secondary_actors": ["Payment provider"], "goal": "Pay less for the basket with a voucher the member holds", "trigger": "The member opens the checkout with a voucher in their wallet", "preconditions": "<p>The member is signed in and the basket is not empty.</p>", "postconditions": "<p>The voucher is marked used and the order carries the discount.</p>", "frequency": "A few hundred times a day in a campaign week", "priority": "must", "status": "approved", "steps": [ { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "use_case_id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091", "position": 0, "flow": "main", "flow_label": null, "actor_action": "The member opens the checkout", "system_response": "The system shows the basket and the vouchers in the wallet", "branches_from": null }, { "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "use_case_id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091", "position": 1, "flow": "main", "flow_label": null, "actor_action": "The member picks a voucher", "system_response": "The system applies the discount and shows the new total", "branches_from": null }, { "id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "use_case_id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091", "position": 2, "flow": "exception", "flow_label": "2a", "actor_action": "The member picks a voucher that expired", "system_response": "The system refuses it, says why and keeps the total", "branches_from": 2 } ], "position": 3, "version": 0, "approved_at": "2026-11-05T15:00:00+00:00", "approved_revision_id": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-05T15:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown use_case_id, a use case of a project outside the token's scope or the person's visibility, or a draft read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks use-case:view. ## Example prompts > "Walk me through CRM-UC-4 step by step, branches included." > "Read use case `e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091` and list every exception flow a tester must cover." > "What must be true before the voucher use case starts, and what is true after?" ## Related - [list_use_cases](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-use-cases) - [get_persona](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/get-persona) — the actor behind actor_persona_id. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — every version of the use case, steps included. - [Use Cases API](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases) --- # Planning Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning The planning record of a project: its epics (the bodies of work the client recognises, each numbered from the project's tracker sequence, `CRM-12`, with its goal, value, measures, owner, MoSCoW priority, status from draft to done and dates) and its requirements (what the system shall do, functional, `CRM-FR-12`, and the qualities it shall keep, non-functional, `CRM-NFR-3`, each with a category and a measure, with the statement, the source, the priority, the verification method and the status from draft to baselined). The business rules sit beside them: the decisions of the business the system must honour (`CRM-BR-7`), each a constraint, derivation, computation, inference, action enabler or policy in plain words, with when it applies and what follows, exceptions, worked examples, authority, priority and the dates in force. The use cases sit beside them too: how a persona reaches a goal with the system (`CRM-UC-4`), with the actor and the other actors, the goal, the trigger, the conditions before and after, how often it happens, priority, status and the steps of the main flow with the alternate and exception flows that leave it. The RAID log keeps what may go wrong and what the project leans on (`CRM-RSK-4`): the risks scored by probability and impact with their mitigation and contingency, the assumptions, the issues and the dependencies, each with its area, owner, dates and status; the log is internal and never read by a guest token. The decisions record every choice the project made (`CRM-DEC-5`): the context, the options weighed with their pros and cons, what was decided, what follows, who decided, when, and the earlier decision each replaces, moving from proposed to accepted or rejected and from accepted to superseded or deprecated; they are internal too. The links join any two of these artefacts, and the dictionary's entities and terms, with a kind read from the source (a story `implements` a requirement, a bug `is fixed by` a story, a decision `relates to` an epic) and read back from the target (`is implemented by`), so one record is followed in both directions; they are internal as well. The comments carry the conversation on each of them: a question, an answer under it, a finding, a decision taken in the margin, each open to everyone who sees the artefact or internal and kept from client guests, naming people with `@` to tell them, and resolved when the question is settled. The attachments are the files pinned on each of them, wireframes, specifications and samples, kept on a private disk and streamed only to a token that may read the artefact. These tools read the epics, the requirements, the rules, the use cases, the log, the decisions, the links between them, the comments on them and the files pinned on them as the token sees them (a guest token the agreed ones only), draft a story into the backlog under an epic with its sentence and its acceptance criteria, a requirement into the specification, a rule into the rulebook, a risk onto the log and a decision as proposed (drafts a person reviews), link two artefacts with a kind (a repeat is refused, never doubled), post a comment in Markdown on any of them (the people named and the followers are told), read the files pinned on any of them and fetch their bytes over REST, and read every version a save of an epic, a work item, a requirement, a rule, a use case, a RAID item or a decision wrote, with who wrote it, why and the diff from the version before, putting a version back when a person asks; create an epic (`create_epic`), a use case with its flows (`create_use_case`) and any RAID item (`create_raid_item`), change every one of these records in part and move it where the lifecycle allows by hand (`update_epic`, `update_requirement`, `update_business_rule`, `update_use_case`, `update_raid_item`, `update_decision`; the review states go through `submit_for_review` and the approvers), delete one or remove a link after a confirmation, hand a brief to Heimdall's reader (`start_brief_import`), read the queue it fills (`list_import_batches`, `get_import_batch`) and decide a row or a council finding on the person's word (`decide_suggestion`, `decide_council_finding`), start the research sweep and read its notes (`start_project_research`, `list_research_notes`), and write who approves what (`update_approval_policy`). Editing, resolving and deleting a comment, uploading and removing a file, and deciding an approval stay in the application and the REST API. Heimdall's quality reviews of a story, a requirement or a rule are read here too (`list_quality_reviews`: the score out of 100, its band and the findings, for an agent to read before it builds or tests a record) and asked for (`review_quality`, which spends the team's AI budget and never changes the record). The baselines and the change requests close the circle: a baseline is a named set of approved records frozen together (the scope the client signs, what a release ships, what a sprint commits to), read with `list_baselines` and `get_baseline` (each record it holds, the version pinned and whether it is still locked), composed as a draft with `create_baseline`, recomposed or superseded with `update_baseline`, deleted while a draft with `delete_baseline` and frozen through `submit_for_review`; a change request (`CRM-CR-2`) is the one way a frozen record changes, read with `list_change_requests` and `get_change_request`, raised with `raise_change_request` naming the baseline and the records it touches, changed or moved by hand with `update_change_request`, read by Heimdall for its impact with `analyse_change_impact` (which spends the team's AI budget and fills only the numbers left empty), deleted while never agreed with `delete_change_request`, and put to its approvers through `submit_for_review`, whose approval unlocks the records it names. A record a frozen baseline holds answers every write with `BASELINE_LOCKED` until then. ## Tools - [`analyse_change_impact`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/analyse-change-impact) — Analyse Change Impact (destructive) - [`comment_on_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/comment-on-item) — Comment On Item (write) - [`convene_council`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — Convene Council (write) - [`create_baseline`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-baseline) — Create Baseline (destructive) - [`create_epic`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-epic) — Create Epic (destructive) - [`create_raid_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-raid-item) — Create RAID Item (destructive) - [`create_requirement_draft`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-requirement-draft) — Create Requirement Draft (write) - [`create_story_draft`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-story-draft) — Create Story Draft (write) - [`create_use_case`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-use-case) — Create Use Case (destructive) - [`decide_council_finding`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-council-finding) — Decide Council Finding (destructive) - [`decide_suggestion`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-suggestion) — Decide Suggestion (destructive) - [`delete_baseline`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-baseline) — Delete Baseline (destructive) - [`delete_business_rule`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-business-rule) — Delete Business Rule (destructive) - [`delete_change_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-change-request) — Delete Change Request (destructive) - [`delete_decision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-decision) — Delete Decision (destructive) - [`delete_epic`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-epic) — Delete Epic (destructive) - [`delete_raid_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-raid-item) — Delete RAID Item (destructive) - [`delete_requirement`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-requirement) — Delete Requirement (destructive) - [`delete_use_case`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-use-case) — Delete Use Case (destructive) - [`get_approval_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-approval-request) — Get Approval Request (read) - [`get_baseline`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-baseline) — Get Baseline (read) - [`get_business_rule`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-business-rule) — Get Business Rule (read) - [`get_change_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-change-request) — Get Change Request (read) - [`get_council_session`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-council-session) — Get Council Session (read) - [`get_decision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-decision) — Get Decision (read) - [`get_epic`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-epic) — Get Epic (read) - [`get_import_batch`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-import-batch) — Get Import Batch (read) - [`get_raid_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-raid-item) — Get RAID Item (read) - [`get_requirement`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-requirement) — Get Requirement (read) - [`get_revision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-revision) — Get Revision (read) - [`get_use_case`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-use-case) — Get Use Case (read) - [`link_artefacts`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/link-artefacts) — Link Artefacts (write) - [`list_attachments`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-attachments) — List Attachments (read) - [`list_baselines`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-baselines) — List Baselines (read) - [`list_business_rules`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-business-rules) — List Business Rules (read) - [`list_change_requests`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-change-requests) — List Change Requests (read) - [`list_comments`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-comments) — List Comments (read) - [`list_council_sessions`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-council-sessions) — List Council Sessions (read) - [`list_decisions`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-decisions) — List Decisions (read) - [`list_epics`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-epics) — List Epics (read) - [`list_import_batches`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-import-batches) — List Import Batches (read) - [`list_links`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-links) — List Links (read) - [`list_pending_approvals`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-pending-approvals) — List Pending Approvals (read) - [`list_quality_reviews`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-quality-reviews) — List Quality Reviews (read) - [`list_raid_items`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-raid-items) — List RAID Items (read) - [`list_requirements`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements) — List Requirements (read) - [`list_research_notes`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-research-notes) — List Research Notes (read) - [`list_revisions`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — List Revisions (read) - [`list_use_cases`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-use-cases) — List Use Cases (read) - [`propose_business_rule`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/propose-business-rule) — Propose Business Rule (write) - [`raise_change_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-change-request) — Raise Change Request (destructive) - [`raise_risk`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-risk) — Raise Risk (write) - [`record_decision_draft`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/record-decision-draft) — Record Decision Draft (write) - [`remove_link`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/remove-link) — Remove Link (destructive) - [`restore_revision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/restore-revision) — Restore Revision (destructive) - [`review_quality`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/review-quality) — Review Quality (write) - [`start_brief_import`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-brief-import) — Start Brief Import (destructive) - [`start_project_research`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-project-research) — Start Project Research (destructive) - [`submit_for_review`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — Submit For Review (destructive) - [`sweep_improvements`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/sweep-improvements) — Sweep Improvements (write) - [`update_approval_policy`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-approval-policy) — Update Approval Policy (destructive) - [`update_baseline`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-baseline) — Update Baseline (destructive) - [`update_business_rule`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-business-rule) — Update Business Rule (destructive) - [`update_change_request`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-change-request) — Update Change Request (destructive) - [`update_decision`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-decision) — Update Decision (destructive) - [`update_epic`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-epic) — Update Epic (destructive) - [`update_raid_item`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-raid-item) — Update RAID Item (destructive) - [`update_requirement`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-requirement) — Update Requirement (destructive) - [`update_use_case`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-use-case) — Update Use Case (destructive) --- # link_artefacts Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/link-artefacts Join two artefacts of a project with a kind read from the source, such as a story that implements a requirement; a repeat, a self-link or a foreign end is refused, and link.created goes out. Join two artefacts of one project: `source_type` and `source_id`, `target_type` and `target_id` (each type one of `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`), a `kind` read from the source (`implements`, `derives_from`, `verifies`, `covers`, `depends_on`, `blocks`, `relates_to`, `duplicates`, `caused_by`, `fixed_by` or `supersedes`; `relates_to` when left out) and an optional `note` of up to 300 characters. The link is returned with both ends named, and `link.created` goes out. Link a story to the requirement it implements and the rule it honours when you pick it up, a bug to the story that fixes it, a decision to the epic it shapes, so the record is true without anyone redrawing it. Both ends must be artefacts of the same project and not the same artefact, and the same kind joins two artefacts once (`relates_to` and `duplicates` read the same from both sides, so the reverse direction counts as the same link): a repeat is refused rather than doubled, which makes the tool safe to call again. Removing a link stays with people. The links are internal: a guest token is refused. - Requires ability: `link:create` - Runs the same action as [`POST /v1/projects/{project}/links`](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/link-two-artefacts) - Fires events: [`link.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-created) - Annotations: Idempotent ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project both artefacts belong to. | | `source_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The source artefact's type. | | `source_id` | string | yes | UUID of the source artefact. | | `target_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The target artefact's type. | | `target_id` | string | yes | UUID of the target artefact. | | `kind` | `implements`, `derives_from`, `verifies`, `covers`, `depends_on`, `blocks`, `relates_to`, `duplicates`, `caused_by`, `fixed_by`, `supersedes` | no | How the source relates to the target; relates_to by default. | | `note` | string | no | A few words on why, up to 300 characters. | ## Example call ```json { "name": "link_artefacts", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "source_type": "epic", "source_id": "a80460a0-bf12-4000-a633-1919cc938260", "target_type": "epic", "target_id": "be7a6110-44ed-4c00-a04d-18a97432ba70" } } ``` ## What it returns ```json { "data": { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "source_type": "work-item", "source_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "source_reference": "CRM-142", "source_title": "Apply a voucher at checkout", "target_type": "requirement", "target_id": "3a4b5c6d-7e8f-4a9b-8c0d-1e2f3a4b5c6d", "target_reference": "CRM-FR-12", "target_title": "The checkout accepts a voucher", "kind": "implements", "phrase": "implements", "inverse_phrase": "is implemented by", "note": "The voucher flow is where the rule bites", "created_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "created_at": "2026-10-28T09:20:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a missing or unknown type, id or kind; the two ends are the same artefact; an end belongs to another project; the same link already exists; a note over 300 characters. - `RESOURCE_NOT_FOUND` — unknown project_id, or an end the token may not see. - `FORBIDDEN` — a guest token: the links are internal. - `TOKEN_MISSING_ABILITY` — token lacks link:create. ## Example prompts > "Link CRM-142 to CRM-FR-12 as implements; the voucher flow is where the rule bites." > "Record that bug CRM-150 is fixed by story CRM-142." > "Mark decision CRM-DEC-5 as relating to the loyalty epic." ## Related - [list_links](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-links) — read what is already linked before you add. - [create_story_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-story-draft) — the story you then link to its requirement. - [Links API](https://alm-docs.labs.zuselab.dev/api/v1/reference/links) --- # list_attachments Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-attachments The files pinned on one artefact, newest first, each with its name, size, kind, uploader and the REST endpoint that streams its bytes; the agreed record only for a guest token. Return the files pinned on one artefact, newest first: each with its name, size in bytes, MIME type and kind (image, document, spreadsheet, presentation, archive, text or other), who uploaded it and when, and the REST endpoint that streams its bytes to the same token. Read it when a story, a requirement or a decision names a wireframe, a specification or a sample, so you fetch the right file rather than guess at it; the MCP transport carries JSON, so the bytes come over REST. The artefact is named by `artefact_type` (`epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`) and `artefact_id`. A guest token reads the files on the agreed record only. Uploading and removing files stays with people and the REST API. - Requires ability: `attachment:view-any` - Runs the same action as [`GET /v1/projects/{project}/attachments`](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments/list-an-artefacts-attachments) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type. | | `artefact_id` | string | yes | UUID of the artefact whose attachments to list. | ## Example call ```json { "name": "list_attachments", "arguments": { "artefact_type": "epic", "artefact_id": "5683b5a5-ee5e-4400-a374-b33e4e067dc7" } } ``` ## What it returns ```json { "data": [ { "id": "c9d0e1f2-a3b4-4c5d-9e6f-7a8b9c0d1e23", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "artefact_type": "work-item", "artefact_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "name": "checkout-wireframe.png", "size": 184320, "mime": "image/png", "kind": "image", "uploaded_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "uploader_name": "Jane Doe", "download_url": "https://api.example.test/v1/attachments/c9d0e1f2-a3b4-4c5d-9e6f-7a8b9c0d1e23/download", "created_at": "2026-10-28T09:20:00+00:00" } ] } ``` ## How it fails - `VALIDATION_FAILED` — artefact_type is not one of the nine; the response lists them. - `RESOURCE_NOT_FOUND` — unknown artefact_id, an artefact of a project outside the token's scope or the person's visibility, or an artefact a guest token may not read. - `FORBIDDEN` — the token holder's role on the project does not carry attachment:view-any. - `TOKEN_MISSING_ABILITY` — token lacks attachment:view-any. ## Example prompts > "What files are attached to CRM-142, and which one is the wireframe?" > "Is there a specification pinned on requirement `3a4b5c6d-7e8f-4a9b-8c0d-1e2f3a4b5c6d`, and how big is it?" > "List the samples on the Order entity before I write the import." ## Related - [list_comments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-comments) — the discussion beside the files. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — the story the files usually hang on. - [Attachments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments) --- # list_baselines Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-baselines Lists a project's baselines in the project's order with their kind, status, version, clocks and record count, narrowed by kind, status or words; a guest token sees the frozen baselines only. Read the baselines of a project before you touch its planning record. Each row is a named set of approved records frozen together, with `kind` (`scope`, `release`, `sprint` or `milestone`), `status` from `draft` to `superseded`, `frozen` and `locks_items`, the `version` (0 while never approved, 1 once the team froze it, 2 once the client signed), the clocks and `item_count`. Narrow with `kind`, `status` or `search` (words the name must contain) and page with `limit` and `page`. A record a frozen baseline holds is locked: an edit, a move, a delete or a restore of it is refused with `BASELINE_LOCKED` until an approved change request unlocks it, so read the baselines first and raise the change request rather than retrying the write. A client guest's token sees the frozen baselines only; the records a baseline holds come with `get_baseline`. - Requires ability: `baseline:view-any` - Runs the same action as [`GET /v1/projects/{project}/baselines`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/list-baselines) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose baselines to list. | | `kind` | `scope`, `release`, `sprint`, `milestone` | no | Narrow to one kind. | | `status` | `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off`, `superseded` | no | Narrow to one status. | | `search` | string | no | Words the name must contain. | | `limit` | integer | no | Baselines per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_baselines", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "v1.0 scope", "kind": "scope", "description": "<p>Everything the client signed for the first release.</p>", "status": "signed_off", "frozen": true, "locks_items": true, "version": 2, "approved_at": "2026-10-29T10:12:00+00:00", "approved_revision_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "frozen_at": "2026-10-27T15:40:00+00:00", "frozen_by": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "signed_off_at": "2026-10-29T10:12:00+00:00", "item_count": 14, "position": 0, "created_at": "2026-10-27T09:00:00+00:00", "updated_at": "2026-10-29T10:12:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 2, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — unknown kind or status (the response lists the supported values), or limit outside 1..100. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope. - `FORBIDDEN` — the holder's role on the project does not carry baseline:view-any. - `TOKEN_MISSING_ABILITY` — token lacks baseline:view-any. ## Example prompts > "Which baselines does the Customer Portal have, and which one did the client sign?" > "List the frozen baselines of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` before I change the reset requirement." > "Is there a release baseline for 1.1 yet?" ## Related - [get_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-baseline) — one baseline with the records it holds and whether each is still locked. - [list_change_requests](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-change-requests) — the requests raised against a baseline. - [list_requirements](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements) — a baselined requirement is one a frozen baseline holds. - [Baselines API](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines) — the same list over REST. --- # list_business_rules Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-business-rules A project's business rules in the project's order, with reference, name, kind, statement, condition and action, examples, authority, priority, dates in force, status and owner, narrowed for a guest token. Return the business rules of one project: the decisions of the business the system must honour, each with its reference (such as `CRM-BR-7`), name, kind (a constraint, a derivation, a computation, an inference, an action enabler or a policy), the rule in plain words, when it applies and what follows, the exceptions, worked examples of an input and what is expected, who has the authority to change it, its MoSCoW priority, the dates it is in force, its status and owner. Read it before you implement a calculation, a validation or a decision, so the code follows the rule the business agreed rather than a guess, and name rules by their reference when you report. Optional filters narrow the page to one `type`, one `status`, one `priority`, one `owner_id` or the rules whose reference, name or authority contain `search`. A guest token reads the agreed rules only (approved or baselined). - Requires ability: `business-rule:view-any` - Runs the same action as [`GET /v1/projects/{project}/business-rules`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-business-rules) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose rules to list. | | `type` | `constraint`, `derivation`, `computation`, `inference`, `action_enabler`, `policy` | no | Narrow to one kind. | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | no | Narrow to one status. | | `priority` | `must`, `should`, `could`, `wont` | no | Narrow to one MoSCoW priority. | | `owner_id` | string | no | Narrow to the rules one person answers for, by user UUID. | | `search` | string | no | Words the reference, the name or the authority must contain. | | `limit` | integer | no | Rules per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_business_rules", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "d5e6f7a8-b9c0-4d1e-8f2a-3b4c5d6e7f80", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 7, "reference": "CRM-BR-7", "name": "Platinum tier threshold", "type": "inference", "statement": "<p>A member whose spend over the last twelve months reaches 5,000 is a platinum member.</p>", "condition": "When a member's rolling twelve-month spend reaches 5,000", "action": "then the member moves to the platinum tier on the first of the next month", "exceptions": "<p>Spend refunded within thirty days does not count.</p>", "examples": [ { "input": "Spend 4,999 on 30 November", "expected": "Stays gold" }, { "input": "Spend 5,000 on 30 November", "expected": "Platinum from 1 December" } ], "authority_stakeholder_id": "d6e7f8a9-b0c1-4d2e-9f3a-4b5c6d7e8f90", "authority_stakeholder_name": "Amal Jayasuriya", "authority": "Loyalty programme terms, section 4", "priority": "must", "effective_from": "2027-01-01", "effective_to": null, "status": "approved", "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "owner_name": "Nimal Fernando", "position": 6, "version": 0, "approved_at": "2026-11-05T15:00:00+00:00", "approved_revision_id": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-05T15:00:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — type, status or priority is not one of its values. - `TOKEN_MISSING_ABILITY` — token lacks business-rule:view-any. ## Example prompts > "What rules decide a member's tier in the customer portal?" > "List the computations of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` and which of them have no worked example." > "Which rules are still drafts, and who owns them?" ## Related - [get_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-business-rule) - [propose_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/propose-business-rule) — add one as a draft. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — how a rule changed, version by version. - [Business Rules API](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules) --- # list_change_requests Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-change-requests Lists a project's change requests in the project's order with their reason, priority, impact level, status, baseline and the people behind them, narrowed by reason, status, priority, impact level, baseline or words; a guest token sees the requests they raised or asked for, without the effort, the cost and Heimdall's draft. Read what was asked for after the record was agreed. Each row is the change request record: `reference` (`CRM-CR-2`), `title`, `reason`, `priority`, `impact_level`, `impact_schedule_days`, `impact_effort_hours`, `impact_cost`, `status` from `submitted` to `closed`, `baseline_id` and `baseline_name`, `requested_by` with `requester_name`, `stakeholder_id` with `stakeholder_name`, the approval stamps and the clocks. Narrow with `reason`, `status`, `priority`, `impact_level`, `baseline_id` (the requests raised against one baseline) or `search` (words the reference or the title must contain), and page with `limit` and `page`. Read it before you change a locked record: an approved request against the baseline that holds it is what unlocked it, and an open one says a change is already on its way. A client guest's token lists only the requests they raised or asked for through their seat, and never the effort, the cost or Heimdall's draft. - Requires ability: `change-request:view-any` - Runs the same action as [`GET /v1/projects/{project}/change-requests`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/list-change-requests) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose change requests to list. | | `reason` | `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt`, `scope_reduction` | no | Narrow to one reason. | | `status` | `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified`, `closed` | no | Narrow to one status. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | Narrow to one priority. | | `impact_level` | `low`, `medium`, `high` | no | Narrow to one impact level. | | `baseline_id` | string | no | UUID of a baseline, to list the requests raised against it. | | `search` | string | no | Words the reference or the title must contain. | | `limit` | integer | no | Change requests per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_change_requests", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 2, "reference": "CRM-CR-2", "title": "Accept vouchers on subscriptions too", "description": "<p>The client wants the voucher applied to the first renewal as well.</p>", "reason": "client_request", "requested_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "requester_name": "Bea Analyst", "stakeholder_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "stakeholder_name": "Gail of the Client", "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "baseline_name": "v1.0 scope", "impact_scope": "<p>CRM-FR-12 gains a renewal clause; CRM-142 and its checkout tests change.</p>", "impact_schedule_days": 5, "impact_effort_hours": "24.00", "impact_cost": "1800.00", "impact_risk": "<p>The renewal job runs unattended; a voucher applied twice would refund twice.</p>", "impact_level": "medium", "ai_impact_draft": null, "analysed_at": null, "priority": "high", "status": "awaiting_approval", "decision_note": null, "verified_by": null, "verified_at": null, "version": 0, "approved_at": null, "approved_revision_id": null, "position": 1, "created_at": "2026-10-29T09:20:00+00:00", "updated_at": "2026-10-29T11:00:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 3, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — unknown reason, status, priority or impact_level (the response lists the supported values), a baseline_id that is not a UUID, or limit outside 1..100. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope. - `FORBIDDEN` — the holder's role on the project does not carry change-request:view-any. - `TOKEN_MISSING_ABILITY` — token lacks change-request:view-any. ## Example prompts > "Which change requests are still open on the Customer Portal, and which of them wait for approval?" > "List the change requests raised against the v1.0 scope." > "Show me the high-impact change requests of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`." ## Related - [get_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-change-request) — one request with the records it touches. - [list_baselines](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-baselines) — the baselines the requests are raised against. - [list_pending_approvals](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-pending-approvals) — the requests awaiting approval, as the inbox lists them. - [Change Requests API](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests) — the same list over REST. --- # list_comments Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-comments The conversation on one artefact, oldest first, each comment with its author, its audience, the people it names, the thread it answers and whether the thread is resolved; the open comments only for a guest token. Return the comments on one artefact, oldest first: each with its author's name, the words as sanitised HTML, whether it is internal, the ids of the people it names, the first comment it answers when a reply, and whether and by whom the thread was resolved. Read it before you act on an artefact: the questions, the answers and the decisions taken in the margin are here, and a thread marked resolved tells you the question is settled. The artefact is named by `artefact_type` (`epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`) and `artefact_id`. A guest token reads the open comments only, and only on the agreed record. - Requires ability: `comment:view-any` - Runs the same action as [`GET /v1/projects/{project}/comments`](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments/list-an-artefacts-comments) - Runs the same action as [`GET /v1/projects/{project}/watchers`](https://alm-docs.labs.zuselab.dev/api/v1/reference/watchers/list-an-artefacts-followers) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type. | | `artefact_id` | string | yes | UUID of the artefact whose comments to list. | ## Example call ```json { "name": "list_comments", "arguments": { "artefact_type": "epic", "artefact_id": "5683b5a5-ee5e-4400-a374-b33e4e067dc7" } } ``` ## What it returns ```json { "data": [ { "id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "artefact_type": "work-item", "artefact_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "parent_id": null, "author_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "author_name": "Jane Doe", "body": "<p>Does the voucher apply before or after the loyalty discount? @Sam Lee</p>", "internal": false, "mentions": ["d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70"], "resolved": false, "resolved_at": null, "resolved_by": null, "edited_at": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-10-28T09:20:00+00:00" }, { "id": "f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f7a8b92", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "artefact_type": "work-item", "artefact_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "parent_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "author_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "author_name": "Sam Lee", "body": "<p>After it: CRM-BR-7 says the voucher comes last.</p>", "internal": false, "mentions": [], "resolved": false, "resolved_at": null, "resolved_by": null, "edited_at": null, "created_at": "2026-10-28T10:05:00+00:00", "updated_at": "2026-10-28T10:05:00+00:00" } ] } ``` ## How it fails - `VALIDATION_FAILED` — artefact_type is not one of the nine; the response lists them. - `RESOURCE_NOT_FOUND` — unknown artefact_id, an artefact of a project outside the token's scope or the person's visibility, or an artefact a guest token may not read. - `FORBIDDEN` — the token holder's role on the project does not carry comment:view-any. - `TOKEN_MISSING_ABILITY` — token lacks comment:view-any. ## Example prompts > "What has been said about CRM-142 so far, and is anything still open?" > "Read the comments on requirement `3a4b5c6d-7e8f-4a9b-8c0d-1e2f3a4b5c6d` and summarise the questions nobody answered." > "Before I implement this story, were there any decisions taken in its discussion?" ## Related - [comment_on_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/comment-on-item) — answer in the thread. - [list_links](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-links) — the other panel of the flyout. - [Comments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments) --- # list_council_sessions Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-council-sessions The sittings of Heimdall's council on a project, newest first, each with its moment, record, voices, status, verdict, counts, cost and turns; free to call. Return every time Heimdall's council sat on a project, newest first: each sitting with its moment, the record it read (by alias, id and title), the voices that sat, where it stands (`queued`, `running`, `summarising`, `ready` or `failed`), Heimdall's verdict once written (the summary, the findings ranked by id with a reason each, and where the voices disagreed), how many findings the voices left, what it cost, who convened it and when, and the voices' turns with their stances. Read it before convening the council on a record it already sat on, and read one sitting in full, findings included, with `get_council_session`. The project is named by `project_id`. The list is empty for a project the council never sat on. Free to call; `convene_council` is the one that spends. A guest token is refused. - Requires ability: `import:view-any` - Runs the same action as [`GET /v1/projects/{project}/council`](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/list-a-projects-council-sessions) - Runs the same action as [`GET /v1/projects/{project}/imports`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-a-projects-imports) - Runs the same action as [`GET /v1/projects/{project}/research`](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research) - Runs the same action as [`GET /v1/projects/{project}/suggestions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research-suggestions) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | ## Example call ```json { "name": "list_council_sessions", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "moment": "requirement_review", "subject_type": "work-item", "subject_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "subject_title": "Apply a voucher at checkout", "status": "ready", "voices": ["thor", "loki"], "round": 3, "phase": "verdict", "summary": "The story is sound; one criterion cannot be verified and the voucher code is personal data the record does not say how to keep.", "plan": "1. Name the limit on the voucher criterion.\n2. Add the masking rule.", "ranked": [ { "finding_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "reason": "A tester is blocked until the limit is named." } ], "disagreements": [], "finding_count": 2, "cost_micros": 12500, "cost": "$0.0125", "failure_reason": null, "requested_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "requester_name": "Jane Doe", "started_at": "2026-10-28T15:00:05+00:00", "finished_at": "2026-10-28T15:03:00+00:00", "created_at": "2026-10-28T15:00:00+00:00", "turns": [ { "id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c03", "session_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "voice": "thor", "voice_name": "Thor", "lens": "Testing, verification and defects", "round": 1, "kind": "reading", "status": "ready", "stance": "One criterion cannot be verified as written.", "sources": null, "finding_count": 1, "failure_reason": null, "started_at": "2026-10-28T15:00:05+00:00", "finished_at": "2026-10-28T15:01:10+00:00" } ] } ] } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry import:view-any. - `TOKEN_MISSING_ABILITY` — token lacks import:view-any. ## Example prompts > "Has the council sat on anything in the Customer Portal this week? List the sittings and their verdicts." > "Before I convene the council on CRM-142, check whether it already sat on it and what it concluded." > "Which sittings on the CRM project failed, and why?" ## Related - [get_council_session](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-council-session) — one sitting with every finding. - [convene_council](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — convene a new sitting. - [Council API](https://alm-docs.labs.zuselab.dev/api/v1/reference/council) — the same list over REST. --- # list_decisions Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-decisions A project's decisions in the project's order, with reference, kind, title, context, the options weighed, what was decided, consequences, status, the day decided, deciders and the decision each supersedes; never for a guest token. Return the decisions of one project: the record of every choice it made, each with its reference (such as `CRM-DEC-5`), kind (architecture, product, process, vendor or security), title, the context it was taken in, the options weighed with their pros and cons, what was decided, what follows, status, the day decided, who decided and the earlier decision it replaces. Read the accepted decisions before you design or build, so the code follows what the team already chose, and name decisions by their reference when you report. Optional filters narrow the page to one `kind`, one `status` or the decisions whose reference or title contain `search`. The log is internal: a guest token is refused. - Requires ability: `decision:view-any` - Runs the same action as [`GET /v1/projects/{project}/decisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/list-decisions) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose decisions to list. | | `kind` | `architecture`, `product`, `process`, `vendor`, `security` | no | Narrow to one kind. | | `status` | `proposed`, `accepted`, `rejected`, `superseded`, `deprecated` | no | Narrow to one status. | | `search` | string | no | Words the reference or the title must contain. | | `limit` | integer | no | Decisions per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_decisions", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 5, "reference": "CRM-DEC-5", "kind": "architecture", "title": "Keep the loyalty ledger in Postgres", "context": "<p>The ledger must be queried with the orders and audited.</p>", "options": [ { "title": "A ledger table in the main Postgres database", "pros": "One backup, one transaction, one place to audit", "cons": "Table growth needs partitioning within two years" }, { "title": "A dedicated event store", "pros": "Append-only by design", "cons": "A second system to run and to secure" } ], "decision": "<p>The ledger lives in Postgres, partitioned by month from the first release.</p>", "consequences": "<p>The reporting queries join the ledger directly.</p>", "status": "accepted", "decided_on": "2026-11-04", "deciders": ["c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f"], "supersedes_id": "b9c0d1e2-f3a4-4b5c-8d6e-7f8a9b0c1d24", "supersedes_reference": "CRM-DEC-2", "position": 4, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-04T15:00:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the person's role on the project does not carry decision:view-any. - `VALIDATION_FAILED` — kind or status is not one of its values. - `TOKEN_MISSING_ABILITY` — token lacks decision:view-any. ## Example prompts > "Which architecture decisions are in force on the customer portal?" > "List the proposed decisions of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` and who should decide them." > "Did the team ever decide where the loyalty ledger lives?" ## Related - [get_decision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-decision) - [record_decision_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/record-decision-draft) — record one as proposed. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — how a decision changed, version by version. - [Decisions API](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions) --- # list_epics Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-epics A project's epics in the project's order, with reference, goal, owner, priority, status and dates, narrowed for a guest token. Return the epics of one project, the bodies of work it will deliver, each with its reference (such as `CRM-12`), title, goal, the prose (summary, business value, hypothesis, success metrics), owner, MoSCoW priority, status, dates and colour. Read it before you write, estimate or implement a story, so the work hangs under the right epic and the goal is in front of you, and name epics by their reference when you report. Optional filters narrow the page to one `status`, one `priority`, one `owner_id` or the epics whose reference, title or goal contain `search`. A guest token reads the agreed epics only (approved, in progress or done). - Requires ability: `epic:view-any` - Runs the same action as [`GET /v1/projects/{project}/epics`](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-epics) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose epics to list. | | `status` | `draft`, `in_review`, `approved`, `in_progress`, `done`, `cancelled` | no | Narrow to one status. | | `priority` | `must`, `should`, `could`, `wont` | no | Narrow to one MoSCoW priority. | | `owner_id` | string | no | Narrow to the epics one person answers for, by user UUID. | | `search` | string | no | Words the reference, the title or the goal must contain. | | `limit` | integer | no | Epics per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_epics", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 12, "reference": "CRM-12", "title": "Loyalty tiers", "summary": "<p>Members climb silver, gold and platinum tiers by annual spend.</p>", "goal": "Reward the top fifth of members so they keep buying from the client.", "business_value": null, "hypothesis": "A visible next tier raises repeat purchases within a quarter.", "success_metrics": null, "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "owner_name": "Nimal Fernando", "priority": "must", "status": "approved", "starts_on": "2026-11-02", "due_on": "2027-03-31", "color": "amber", "position": 0, "version": 0, "approved_at": "2026-11-05T15:00:00+00:00", "approved_revision_id": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-05T15:00:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — status or priority is not one of its values. - `TOKEN_MISSING_ABILITY` — token lacks epic:view-any. ## Example prompts > "What epics is the customer portal project delivering, and which are must-haves?" > "List the draft epics of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` and who owns them." > "Which epic should the checkout story hang under?" ## Related - [get_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-epic) - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — how an epic changed, version by version. - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) --- # list_import_batches Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-import-batches Lists the brief imports of a project, newest first, with where each reading stands. List the brief imports of a project by `project_id`: each with its title, source, status (`queued`, `running`, `ready`, `accepted`, `discarded` or `failed`), how many suggestions it produced, which provider and model answered, who started it and when. Read one with `get_import_batch` for the brief's words and the suggestions themselves. A staff token with `import:view-any` on the project is needed. - Requires ability: `import:view-any` - Runs the same action as [`GET /v1/projects/{project}/council`](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/list-a-projects-council-sessions) - Runs the same action as [`GET /v1/projects/{project}/imports`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-a-projects-imports) - Runs the same action as [`GET /v1/projects/{project}/research`](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research) - Runs the same action as [`GET /v1/projects/{project}/suggestions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research-suggestions) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | ## Example call ```json { "name": "list_import_batches", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "f2a3b4c5-d6e7-4f8a-8b9c-0d1e2f3a4b5c", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "source": "pasted_text", "status": "ready", "title": "Laundry shop brief", "excerpt": "An informational website for a laundry shop with order placement, tracking, payment and appointment scheduling…", "suggestion_count": 23, "provider": "anthropic", "model": "claude-sonnet-5-5", "usage_id": "a3b4c5d6-e7f8-4a9b-8c0d-1e2f3a4b5c6d", "failure_reason": null, "created_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "creator_name": "Bea Analyst", "started_at": "2026-10-28T11:20:05+00:00", "finished_at": "2026-10-28T11:21:40+00:00", "created_at": "2026-10-28T11:20:00+00:00", "updated_at": "2026-10-28T11:21:40+00:00" } ], "meta": { "total": 1 } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry import:view-any. - `TOKEN_MISSING_ABILITY` — token lacks import:view-any. ## Example prompts > "Which briefs have been imported into the laundry project?" > "Is the import I started this morning ready?" ## Related - [get_import_batch](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-import-batch) — one batch with its suggestions. - [start_brief_import](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-brief-import) — a new reading. - [Imports API](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports) — the same list over REST. --- # list_links Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-links Every link that touches one artefact, both ends named with their keys and titles, each read from the artefact's side; never for a guest token. Return every link that touches one artefact, newest first, whatever the side it was made from: each with its `kind`, the `phrase` read from the source (such as `implements`) and the `inverse_phrase` read from the target (`is implemented by`), the two ends' types, ids, keys and titles, and the note. Read it before you implement or test an artefact, to find the requirements a story implements, the rules it must honour, the decisions it builds on and the items it depends on or blocks; name the ends by their keys when you report. The artefact is named by `artefact_type` (`epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity` or `glossary-term`) and `artefact_id`. A link whose other end is gone is left out. The links are internal: a guest token is refused. - Requires ability: `link:view-any` - Runs the same action as [`GET /v1/projects/{project}/links`](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/list-an-artefacts-links) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term`, `baseline`, `change-request` | yes | The artefact's type. | | `artefact_id` | string | yes | UUID of the artefact whose links to list. | ## Example call ```json { "name": "list_links", "arguments": { "artefact_type": "epic", "artefact_id": "5683b5a5-ee5e-4400-a374-b33e4e067dc7" } } ``` ## What it returns ```json { "data": [ { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "source_type": "work-item", "source_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "source_reference": "CRM-142", "source_title": "Apply a voucher at checkout", "target_type": "requirement", "target_id": "3a4b5c6d-7e8f-4a9b-8c0d-1e2f3a4b5c6d", "target_reference": "CRM-FR-12", "target_title": "The checkout accepts a voucher", "kind": "implements", "phrase": "implements", "inverse_phrase": "is implemented by", "note": "The voucher flow is where the rule bites", "created_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "created_at": "2026-10-28T09:20:00+00:00" } ] } ``` ## How it fails - `VALIDATION_FAILED` — artefact_type is not one of the nine linkable types; the response lists them. - `RESOURCE_NOT_FOUND` — unknown artefact_id, an artefact of a project outside the token's scope or the person's visibility, or an artefact a guest token may not read. - `FORBIDDEN` — a guest token: the links are internal. - `TOKEN_MISSING_ABILITY` — token lacks link:view-any. ## Example prompts > "What does CRM-142 implement, and which rules does it have to honour?" > "List the links of requirement `3a4b5c6d-7e8f-4a9b-8c0d-1e2f3a4b5c6d` and tell me which stories implement it." > "Before I pick up this story, what does it depend on and what blocks it?" ## Related - [link_artefacts](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/link-artefacts) — join two artefacts. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — the story one end usually is. - [Links API](https://alm-docs.labs.zuselab.dev/api/v1/reference/links) --- # list_pending_approvals Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-pending-approvals Lists approval requests as the inbox shows them, waiting on the token holder by default, raised by them or everything they may see, with filters and paging. Read the approvals inbox. `scope` picks the view: `waiting` (the default) is what still waits for the token holder's own decision, leaving out the rounds they already answered; `raised` is what they submitted; `all` is everything on the projects they may see. A guest token reads only the requests that name them. `project_id` narrows to one project, and `status`, `kind`, `artefact_type` and `search` narrow further. Each row carries the approvers the policy named and a tally of where the approvals stand. Deciding is a person's act: there is no tool for it. Use this to explain to a person what waits on whom and why a record is still in review, then point them at the inbox. - Requires ability: `approval:view-any` - Runs the same action as [`GET /v1/approvals`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-the-inbox) - Runs the same action as [`GET /v1/projects/{project}/approvals`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/list-a-projects-approvals) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `scope` | `waiting`, `raised`, `all` | no | Which view: `waiting` (default) for what waits on the token holder, `raised` for what they submitted, `all` for everything they may see. | | `project_id` | string | no | UUID of a project to narrow to. | | `status` | `pending`, `approved`, `rejected`, `changes_requested`, `withdrawn`, `expired` | no | Narrow to one status. | | `kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | no | Narrow to one kind of approval. | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `glossary-term`, `data-entity`, `persona`, `stakeholder`, `baseline`, `change-request`, `generated-document`, `sprint` | no | Narrow to one family of record under approval. | | `search` | string | no | Words the reference or the title must contain. | | `limit` | integer | no | Requests per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_pending_approvals", "arguments": { "scope": "waiting", "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "approvable_type": "requirement", "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Apply a voucher at checkout", "kind": "requirement_review", "status": "pending", "rule": "all", "quorum": 1, "round": 1, "approvers": [ { "user_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "name": "Pat Manager", "via": "role", "stakeholder_id": null }, { "user_id": "9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a", "name": "Gail of the Client", "via": "stakeholder", "stakeholder_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f" } ], "tally": { "approved": 1, "needed": 2, "total": 2 }, "requires_comment": true, "requested_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "requester_name": "Bea Analyst", "summary": "First cut of the voucher rules.", "revision_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "due_at": "2026-10-31T09:20:00+00:00", "remind_at": "2026-10-29T09:20:00+00:00", "auto_approve_at": null, "reminded_at": null, "escalated_at": null, "decided_at": null, "auto_approved": false, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-10-28T11:05:00+00:00" } ], "meta": { "page": 1, "per_page": 25, "total": 1 } } ``` ## How it fails - `VALIDATION_FAILED` — unknown scope, status, kind or artefact_type. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the holder's role on the project named does not carry approval:view-any. - `TOKEN_MISSING_ABILITY` — token lacks approval:view-any. ## Example prompts > "What approvals are waiting on me?" > "List everything I submitted for review on the Customer Portal and tell me which are still pending." > "Which requirements on project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` are waiting for the client's sign-off?" ## Related - [get_approval_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-approval-request) — one request with every verdict. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — put a record up for approval. - [Approvals API](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals) — the inbox over REST. --- # list_quality_reviews Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-quality-reviews The quality reviews Heimdall wrote of one work item, requirement or business rule, newest first, each with the version it read, its score, band, summary and findings; free to call. Return every quality review Heimdall wrote of one record, newest first: each with the version of the record it read, the score and its band, a summary and the findings with their codes, severities, the words concerned and the suggestions. Read the first review before you implement or test a record, and compare its `version` with the record's current version from `list_revisions`: a review of an older version may be stale, and only `review_quality` spends the budget to refresh it. The record is named by `artefact_type` (`work-item`, `requirement` or `business-rule`) and `artefact_id`. The list is empty for a record never reviewed. A guest token is refused. - Requires ability: `activity:read` - Runs the same action as [`GET /v1/business-rules/{rule}/quality-reviews`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/list-a-business-rules-quality-reviews) - Runs the same action as [`GET /v1/requirements/{requirement}/quality-reviews`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-a-requirements-quality-reviews) - Runs the same action as [`GET /v1/work-items/{item}/quality-reviews`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/list-a-work-items-quality-reviews) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `work-item`, `requirement`, `business-rule` | yes | The record's type: a work item, a requirement or a business rule. | | `artefact_id` | string | yes | UUID of the record whose reviews to list. | ## Example call ```json { "name": "list_quality_reviews", "arguments": { "artefact_type": "work-item", "artefact_id": "5683b5a5-ee5e-4400-a374-b33e4e067dc7" } } ``` ## What it returns ```json { "data": [ { "id": "b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "artefact_type": "work-item", "artefact_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "version": 3, "score": 71, "band": "needs_work", "summary": "A clear story with a measurable goal; one criterion cannot be verified as written and one word leaves the scope open.", "findings": [ { "code": "testable", "severity": "high", "message": "The criterion \"the basket updates quickly\" cannot pass or fail: \"quickly\" has no limit.", "suggestion": "Then the basket total updates within 1 second of the voucher being applied.", "field": "criteria" } ], "findings_count": 1, "provider": "anthropic", "model": "the model the quality role is configured with", "usage_id": "c9d0e1f2-a3b4-4c5d-8e6f-7a8b9c0d1e23", "reviewed_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "reviewer_name": "Jane Doe", "reviewed_at": "2026-10-28T11:00:00+00:00", "created_at": "2026-10-28T11:00:00+00:00" }, { "id": "a6b7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c90", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "artefact_type": "work-item", "artefact_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "version": 1, "score": 40, "band": "not_ready", "summary": "The story has no acceptance criteria and reads as an epic.", "findings": [ { "code": "missing_criteria", "severity": "high", "message": "The story has no acceptance criteria.", "suggestion": "Add one criterion per outcome: the voucher applied, refused when expired, refused twice.", "field": "criteria" }, { "code": "small", "severity": "high", "message": "\"Apply vouchers, gift cards and loyalty points at checkout\" is three stories.", "suggestion": "Split it: one story per tender kind.", "field": "title" }, { "code": "ambiguous", "severity": "medium", "message": "\"Some vouchers\" leaves open which vouchers apply.", "suggestion": "Name the voucher kinds.", "field": "description" } ], "findings_count": 3, "provider": "anthropic", "model": "the model the quality role is configured with", "usage_id": "d0e1f2a3-b4c5-4d6e-8f7a-8b9c0d1e2f34", "reviewed_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "reviewer_name": "Jane Doe", "reviewed_at": "2026-10-27T15:30:00+00:00", "created_at": "2026-10-27T15:30:00+00:00" } ] } ``` ## How it fails - `VALIDATION_FAILED` — artefact_type is not work-item, requirement or business-rule; the response lists them. - `RESOURCE_NOT_FOUND` — unknown artefact_id, or a record of a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's. - `TOKEN_MISSING_ABILITY` — token lacks activity:read. ## Example prompts > "What did the last quality review of CRM-142 find, and is it still current?" > "List the open quality findings on the requirements I am about to test." > "Has business rule CRM-BR-7 ever scored below 50, and what was wrong?" ## Related - [review_quality](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/review-quality) — ask for a fresh review. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — the record's current version, to tell a stale review. - [Work items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) — the same list under each record's quality reviews endpoint. - [requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements) — the same list under each record's quality reviews endpoint. - [business rules API](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules) — the same list under each record's quality reviews endpoint. --- # list_raid_items Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-raid-items A project's RAID log in the project's order, with reference, kind, title, area, probability, impact and score, mitigation and contingency, owner and raiser, dates, dependency and status; never for a guest token. Return the RAID log of one project: the risks (what may happen, how likely and how bad, with the mitigation and the contingency), the assumptions the plan rests on, the issues hurting now and the dependencies on others, each with its reference (such as `CRM-RSK-4`, whatever the kind), kind, title, description, area, the ratings and the score they give, owner and raiser, due or review date, what a dependency depends on and its status. Read it before you plan, size or promise a date, so the risks and the dependencies the team already knows shape the answer, and name items by their reference when you report. Optional filters narrow the page to one `type`, one `status`, one `category`, the items one `owner_id` owns, or the items whose reference, title or dependency contain `search`. The log is internal: a guest token is refused. - Requires ability: `raid:view-any` - Runs the same action as [`GET /v1/projects/{project}/raid-items`](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/list-raid-items) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose log to list. | | `type` | `risk`, `assumption`, `issue`, `dependency` | no | Narrow to one kind. | | `status` | `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred`, `accepted` | no | Narrow to one status. | | `category` | `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality`, `other` | no | Narrow to one area of the project. | | `owner_id` | string | no | Narrow to the items one person owns, by user UUID. | | `search` | string | no | Words the reference, the title or what the item depends on must contain. | | `limit` | integer | no | Items per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_raid_items", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "f7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b02", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 4, "reference": "CRM-RSK-4", "type": "risk", "title": "Payment provider sandbox may lag behind production", "description": "<p>The provider ships new card flows to production weeks before its sandbox.</p>", "category": "external", "probability": 3, "impact": 4, "score": 12, "mitigation": "<p>Run the checkout smoke test against the provider's production account every Monday.</p>", "contingency": "<p>Pin the gateway to the last verified flow and raise an incident with the provider.</p>", "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "owner_name": "Nimal Fernando", "raised_by": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "raised_by_name": "Sithara Perera", "due_on": null, "review_on": "2026-11-16", "depends_on": null, "status": "monitoring", "position": 3, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-02T15:00:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the person's role on the project does not carry raid:view-any. - `VALIDATION_FAILED` — type, status or category is not one of its values. - `TOKEN_MISSING_ABILITY` — token lacks raid:view-any. ## Example prompts > "Which risks on the customer portal score fifteen or more?" > "List the open dependencies of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` and when each is due." > "What is the team assuming about the payment provider?" ## Related - [get_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-raid-item) - [raise_risk](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-risk) — raise a risk as open. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — how an item changed, version by version. - [RAID Log API](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log) --- # list_requirements Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements A project's functional and non-functional requirements in the project's order, with reference, kind, category, statement, source, priority, measure, verification method, status and owner, narrowed for a guest token. Return the requirements of one project: what the system shall do (functional, `CRM-FR-12`) and the qualities it shall keep (non-functional, `CRM-NFR-3`, each with the category it belongs to and the measure that says when it is met), each with its statement, rationale, where it came from (a stakeholder of the project, or words), MoSCoW priority, how it will be shown to be met, status and owner. Read it before you design, build or test, so the behaviour the client agreed and the qualities it is held to are in front of you, and name requirements by their reference when you report. Optional filters narrow the page to one `type`, one `category`, one `status`, one `priority`, one `owner_id` or the requirements whose reference, title or source contain `search`. A guest token reads the agreed requirements only (approved or baselined). - Requires ability: `requirement:view-any` - Runs the same action as [`GET /v1/projects/{project}/requirements`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/list-requirements) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose requirements to list. | | `type` | `functional`, `non_functional` | no | Narrow to one kind. | | `category` | `performance`, `scalability`, `availability`, `reliability`, `security`, `privacy`, `usability`, `accessibility`, `compatibility`, `maintainability`, `portability`, `compliance`, `observability`, `localisation`, `other` | no | Narrow to the non-functional requirements of one category. | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | no | Narrow to one status. | | `priority` | `must`, `should`, `could`, `wont` | no | Narrow to one MoSCoW priority. | | `owner_id` | string | no | Narrow to the requirements one person answers for, by user UUID. | | `search` | string | no | Words the reference, the title or the source must contain. | | `limit` | integer | no | Requirements per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_requirements", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "type": "non_functional", "number": 3, "reference": "CRM-NFR-3", "category": "performance", "title": "Tier page answers within a third of a second", "statement": "<p>The system shall render a member's tier page within 300 ms at the 95th percentile with 500 members browsing at once.</p>", "rationale": null, "source_stakeholder_id": "d6e7f8a9-b0c1-4d2e-9f3a-4b5c6d7e8f90", "source_stakeholder_name": "Amal Jayasuriya", "source": "Discovery workshop, 14 October", "priority": "must", "measure": "p95 under 300 ms at 500 concurrent users, measured on staging", "verification_method": "test", "status": "approved", "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "owner_name": "Nimal Fernando", "position": 2, "version": 0, "approved_at": "2026-11-05T15:00:00+00:00", "approved_revision_id": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-05T15:00:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — type, category, status or priority is not one of its values. - `TOKEN_MISSING_ABILITY` — token lacks requirement:view-any. ## Example prompts > "What must the customer portal do for a member who forgot their password?" > "List the security requirements of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` and which of them have no measure yet." > "Which requirements are still in review, and who owns them?" ## Related - [get_requirement](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-requirement) - [create_requirement_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-requirement-draft) — add one as a draft. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — how a requirement changed, version by version. - [Requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements) --- # list_research_notes Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-research-notes Reads what Heimdall's research sweep found about a project's domain, with the latest run's standing. Read a project's research by `project_id`: `run`, the latest sweep with its status (`queued`, `running`, `ready` or `failed`) and counts, or null when none was started; and `notes`, the notes of the latest finished sweep, each with a topic, a summary and its sources. Read them before drafting requirements or rules so the record carries what the industry expects, and tell a person whether a sweep is still running. A staff token with `import:view-any` on the project is needed. - Requires ability: `import:view-any` - Runs the same action as [`GET /v1/projects/{project}/council`](https://alm-docs.labs.zuselab.dev/api/v1/reference/council/list-a-projects-council-sessions) - Runs the same action as [`GET /v1/projects/{project}/imports`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/list-a-projects-imports) - Runs the same action as [`GET /v1/projects/{project}/research`](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research) - Runs the same action as [`GET /v1/projects/{project}/suggestions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/list-a-projects-research-suggestions) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | ## Example call ```json { "name": "list_research_notes", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": { "run": { "id": "c5d6e7f8-a9b0-4c1d-8e2f-3a4b5c6d7e8f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "status": "ready", "note_count": 6, "suggestion_count": 9, "provider": "anthropic", "model": "claude-sonnet-5-5", "usage_id": "d6e7f8a9-b0c1-4d2e-8f3a-4b5c6d7e8f9a", "failure_reason": null, "created_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "creator_name": "Bea Analyst", "started_at": "2026-10-28T11:50:04+00:00", "finished_at": "2026-10-28T11:53:30+00:00", "created_at": "2026-10-28T11:50:00+00:00" }, "notes": [ { "id": "e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a0b", "run_id": "c5d6e7f8-a9b0-4c1d-8e2f-3a4b5c6d7e8f", "position": 1, "topic": "Compliance", "summary": "Card payments taken on the site fall under PCI DSS; a hosted payment page keeps the site out of scope for most of it.", "sources": [ { "title": "PCI Security Standards Council", "url": "https://www.pcisecuritystandards.org/" } ], "created_at": "2026-10-28T11:53:30+00:00" } ] } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry import:view-any. - `TOKEN_MISSING_ABILITY` — token lacks import:view-any. ## Example prompts > "What did the research find about payments compliance for the laundry project?" > "Is the research sweep finished? Summarise the notes." ## Related - [start_project_research](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-project-research) — a new sweep. - [create_requirement_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-requirement-draft) — the requirements a note implies. - [Research API](https://alm-docs.labs.zuselab.dev/api/v1/reference/research) — the runs and the notes over REST. --- # list_revisions Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions Every version of a revisable artefact, newest first, with who wrote it, why, and the diff from the version before. Return the versions of one artefact, newest first: for each, the version number, the reason it was written (`created`, `edit`, `ai_assist`, `import`, `transition`, `restore`, `approval`, `baseline` or `manual`), who wrote it and through what, a one-line summary, the columns it changed, the diff from the version before and the full snapshot. Read it to see how a record changed before you change it again, to find who last touched a field, and to find the `revision_id` a restore needs. Takes `artefact_type` (`epic` today; the planning families join as they land) and `artefact_id`. Needs `activity:read` and the artefact's own view ability; a guest token is refused, because the versions are the drafts behind the agreed record. - Requires ability: `activity:read` - Runs the same action as [`GET /v1/epics/{epic}/revisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/list-an-epics-versions) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `glossary-term`, `data-entity`, `persona`, `stakeholder`, `baseline`, `change-request`, `generated-document`, `sprint` | yes | The artefact family the versions belong to. | | `artefact_id` | string | yes | UUID of the artefact whose versions to list. | | `limit` | integer | no | Versions per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_revisions", "arguments": { "artefact_type": "epic", "artefact_id": "5683b5a5-ee5e-4400-a374-b33e4e067dc7" } } ``` ## What it returns ```json { "data": [ { "id": "e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a0b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "revisable_type": "epic", "revisable_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "version": 3, "reason": "edit", "actor_kind": "human", "author_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "author_name": "Nimal Fernando", "ai_feature": null, "restored_from_id": null, "restored_from_version": null, "summary": "Edited: Priority, Due on", "changed_fields": ["priority", "due_on"], "diff": { "priority": { "from": "should", "to": "must" }, "due_on": { "from": null, "to": "2027-03-31" } }, "snapshot": { "title": "Loyalty tiers", "summary": null, "goal": "Reward the top fifth of members so they keep buying from the client.", "business_value": null, "hypothesis": null, "success_metrics": null, "owner_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "priority": "must", "status": "draft", "starts_on": null, "due_on": "2027-03-31", "color": null }, "created_at": "2026-10-29T10:15:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 3, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — artefact_type is not a revisable family. - `RESOURCE_NOT_FOUND` — unknown artefact_id, or an artefact outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a guest's, or its holder's role on the project does not carry the artefact's view permission. - `TOKEN_MISSING_ABILITY` — token lacks activity:read. ## Example prompts > "How did the loyalty tiers epic change since it was approved?" > "List the versions of epic `a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d` and tell me who changed the priority." > "Did Heimdall rewrite the goal of CRM-12, and what did it say before?" ## Related - [get_revision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-revision) - [restore_revision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/restore-revision) — put a version back. - [get_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-epic) — the record as it stands. - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) --- # list_use_cases Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-use-cases A project's use cases in the project's order, with reference, title, actor, other actors, goal, trigger, conditions, frequency, priority and status, narrowed for a guest token; the steps come with get_use_case. Return the use cases of one project: how each actor reaches a goal with the system, each with its reference (such as `CRM-UC-4`), title, the persona who acts and the other actors in the room, the goal, the trigger, what must be true before and after, how often it happens, its MoSCoW priority and its status. Read it to find the journey a story or a requirement belongs to before you implement or test it, and name use cases by their reference when you report. The steps are left out of the list and come with `get_use_case`. Optional filters narrow the page to one `status`, one `priority`, the use cases one `persona_id` acts in, or the use cases whose reference, title or goal contain `search`. A guest token reads the approved use cases only. - Requires ability: `use-case:view-any` - Runs the same action as [`GET /v1/projects/{project}/use-cases`](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/list-use-cases) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose use cases to list. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | Narrow to one status. | | `priority` | `must`, `should`, `could`, `wont` | no | Narrow to one MoSCoW priority. | | `persona_id` | string | no | Narrow to the use cases one persona acts in, by persona UUID. | | `search` | string | no | Words the reference, the title or the goal must contain. | | `limit` | integer | no | Use cases per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_use_cases", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 4, "reference": "CRM-UC-4", "title": "Redeem a voucher at checkout", "actor_persona_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "actor_persona_name": "Nadeesha, the returning shopper", "secondary_actors": ["Payment provider"], "goal": "Pay less for the basket with a voucher the member holds", "trigger": "The member opens the checkout with a voucher in their wallet", "preconditions": "<p>The member is signed in and the basket is not empty.</p>", "postconditions": "<p>The voucher is marked used and the order carries the discount.</p>", "frequency": "A few hundred times a day in a campaign week", "priority": "must", "status": "approved", "steps": [], "position": 3, "version": 0, "approved_at": "2026-11-05T15:00:00+00:00", "approved_revision_id": null, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-11-05T15:00:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — status or priority is not one of its values. - `TOKEN_MISSING_ABILITY` — token lacks use-case:view-any. ## Example prompts > "Which use cases does the returning shopper act in?" > "List the must-have use cases of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` and tell me which are still drafts." > "Which use case does the voucher story belong to?" ## Related - [get_use_case](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-use-case) — one use case with its steps. - [list_personas](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/list-personas) — the actors a use case names. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — how a use case changed, version by version. - [Use Cases API](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases) --- # propose_business_rule Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/propose-business-rule Add a business rule to a project as a draft a person reviews, with its kind, statement, condition and action, exceptions, worked examples, authority and priority. Add a business rule to a project: a name, the kind (`constraint` by default), the rule in plain words, when it applies and what follows, the exceptions, worked examples of an input and what is expected, who or what its authority is, and a MoSCoW priority. The rule takes the next number of the rules' sequence (`CRM-BR-7`), starts as a draft and writes its first version; `business_rule.created` goes out. A bounded write: it adds one draft and nothing else, so the person reviews it on the Business Rules page before anyone codes against it. Propose a rule when a story, a requirement or a conversation states a decision of the business that nothing records yet; write it so a developer can code it and a tester can check it, and give at least one worked example, because a rule without an example is argued about and a rule with three is tested. - Requires ability: `business-rule:create` - Runs the same action as [`POST /v1/projects/{project}/business-rules`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/add-a-business-rule) - Fires events: [`business_rule.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-created) ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project the rule belongs to. | | `name` | string | yes | What the rule is called, 3 to 200 characters. | | `type` | `constraint`, `derivation`, `computation`, `inference`, `action_enabler`, `policy` | no | The kind; constraint by default. | | `statement` | string | no | The rule in plain words, as HTML, as the business would say it. | | `condition` | string | no | When the rule applies, in one line, as in "When a member's annual spend crosses the platinum threshold". | | `action` | string | no | What follows when it does, in one line, as in "then the member moves to the platinum tier on the first of the next month". | | `exceptions` | string | no | When the rule does not apply, as HTML. | | `examples` | array of object | no | Worked examples, up to twenty rows, each with an `input` (required) and what is `expected`. | | `examples[].input` | string | yes | The input, up to 500 characters. | | `examples[].expected` | string | no | What the rule gives for it, up to 500 characters. | | `authority` | string | no | Who or what decides the rule, in words: a role, a policy, a regulation. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority; should by default. | ## Example call ```json { "name": "propose_business_rule", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "d5e6f7a8-b9c0-4d1e-8f2a-3b4c5d6e7f80", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 8, "reference": "CRM-BR-8", "name": "Refunds within thirty days", "type": "constraint", "statement": "<p>A member may return an order for a refund within thirty days of delivery.</p>", "condition": "When a return is requested", "action": "then it is accepted only if fewer than thirty days have passed since delivery", "exceptions": "<p>Perishable goods are not returnable.</p>", "examples": [ { "input": "Delivered 1 March, return requested 30 March", "expected": "Accepted" }, { "input": "Delivered 1 March, return requested 1 April", "expected": "Refused" } ], "authority_stakeholder_id": null, "authority_stakeholder_name": null, "authority": "Consumer protection regulations", "priority": "must", "effective_from": null, "effective_to": null, "status": "draft", "owner_id": null, "owner_name": null, "position": 7, "version": 0, "approved_at": null, "approved_revision_id": null, "created_at": "2026-11-03T10:00:00+00:00", "updated_at": "2026-11-03T10:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — the name is missing or too long, an example has no input, or type or priority is not one of its values. - `TOKEN_MISSING_ABILITY` — token lacks business-rule:create. - `FORBIDDEN` — the token holder's role on the project does not carry business-rule:create. ## Example prompts > "The returns story says refunds are accepted within thirty days; propose that as a rule with two examples." > "Add a computation rule to project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` for how the loyalty points are rounded." > "Turn the policy from the workshop notes into a draft rule with its exceptions." ## Related - [list_business_rules](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-business-rules) — see what is there before you add one. - [get_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-business-rule) — read the draft back. - [list_requirements](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-requirements) — the requirements a rule constrains. - [Business Rules API](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules) --- # raise_change_request Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-change-request Raises a change request on a project, numbered from the change requests' own sequence, with its reason, the baseline it changes, the stakeholder who asked, the records it touches and whatever impact is known; a guest token raises one without the impact. Ask for a change to the agreed record. `project_id` and a `title` (3 to 200 characters) are required; `description` says what is asked for and why as HTML, `reason` is one of `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt` or `scope_reduction` (default new requirement), `priority` runs from `lowest` to `highest` (default medium), `baseline_id` names the frozen baseline the change applies to and `stakeholder_id` the client's stakeholder who asked (both must be the project's), and `affected` names the records it touches as objects of `type` (`epic`, `work-item`, `requirement`, `business-rule` or `use-case`) and `id`, each becoming a link from the request. The impact fields (`impact_scope`, `impact_schedule_days`, `impact_effort_hours`, `impact_cost`, `impact_risk`, `impact_level`) may come now or later from the analyst or `analyse_change_impact`. The request is written as `submitted` with the token's holder as the person who raised it; a client guest's token raises it without the impact and with their own seat as the stakeholder when their account is linked to one. Submitting it to the approvers is a person's act, through `submit_for_review` on their word. Runs the same action as **Raise Change Request** in the application and emits `change_request.created`. - Requires ability: `change-request:create` - Runs the same action as [`POST /v1/projects/{project}/change-requests`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/raise-a-change-request) - Fires events: [`change_request.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `title` | string | yes | What the change is called, 3 to 200 characters. | | `description` | string | no | What is asked for and why, as HTML. | | `reason` | `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt`, `scope_reduction` | no | Why it was raised. | | `baseline_id` | string | no | UUID of the baseline the change is raised against. | | `stakeholder_id` | string | no | UUID of the stakeholder who asked, when the client did. | | `affected` | array of any | no | The records it touches: objects with type (epic, work-item, requirement, business-rule or use-case) and id. | | `impact_scope` | string | no | What the change touches, as HTML. | | `impact_schedule_days` | integer | no | Days the schedule moves; negative when it shortens. | | `impact_effort_hours` | number | no | Hours of effort. | | `impact_cost` | number | no | The cost, in the project's currency. | | `impact_risk` | string | no | What the change risks, as HTML. | | `impact_level` | `low`, `medium`, `high` | no | How hard it hits the project. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | How urgent the change is. | ## Example call ```json { "name": "raise_change_request", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>" } } ``` ## What it returns ```json { "data": { "id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 2, "reference": "CRM-CR-2", "title": "Accept vouchers on subscriptions too", "description": "<p>The client wants the voucher applied to the first renewal as well.</p>", "reason": "client_request", "requested_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "requester_name": "Bea Analyst", "stakeholder_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "stakeholder_name": "Gail of the Client", "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "baseline_name": "v1.0 scope", "affected": [ { "artefact_type": "requirement", "artefact_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Password reset by email" } ], "impact_scope": null, "impact_schedule_days": null, "impact_effort_hours": null, "impact_cost": null, "impact_risk": null, "impact_level": null, "ai_impact_draft": null, "analysed_at": null, "priority": "high", "status": "submitted", "decision_note": null, "verified_by": null, "verified_at": null, "version": 0, "approved_at": null, "approved_revision_id": null, "position": 1, "created_at": "2026-10-29T09:20:00+00:00", "updated_at": "2026-10-29T09:20:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — the title is missing, shorter than 3 or longer than 200 characters; an unknown reason, priority or impact_level; a schedule impact outside -3650..3650 days or a negative effort or cost; a baseline_id or stakeholder_id that is not the project's; a record under affected whose type cannot be touched; more than 200 records. - `RESOURCE_NOT_FOUND` — unknown project_id, or a record under affected that is hidden, gone or another project's; nothing is written. - `FORBIDDEN` — the holder's role on the project does not carry change-request:create. - `TOKEN_MISSING_ABILITY` — token lacks change-request:create. ## Example prompts > "The client wants vouchers on renewals too; raise a change request against the signed scope touching CRM-FR-12, asked by Gail." > "Raise a scope-reduction request on project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` to drop the fax confirmation." > "Raise a defect change request for the double refund on cancel, high priority, against the v1.0 scope." ## Related - [analyse_change_impact](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/analyse-change-impact) — Heimdall's reading of the request's impact. - [update_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-change-request) — change the words, the records or the impact, or move it by hand. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — put it to the approvers, on the person's word. - [get_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-baseline) — the records a baseline holds, to name the right ones. - [Change Requests API](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests) — the same write over REST. --- # raise_risk Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-risk Raise a risk in a project's RAID log, numbered from the log's sequence, scored by probability and impact, raised by the person the token acts as and open for the review meeting. Add a risk to a project's RAID log: `title` (required), `description`, `category` (the area it touches, `other` by default), `probability` and `impact` (one to five, how likely and how bad), `mitigation` (what makes it less likely or less bad) and `contingency` (what happens if it occurs). The risk takes the next number of the log's sequence (`CRM-RSK-4`), starts open, records the person the token acts as as its raiser, writes its first version and goes out as `raid_item.created`. Raise a risk when a story, a plan or a conversation shows something that may go wrong and nothing records it yet; say how likely and how bad it is, and what the team can do about it, so the review meeting can act on it. Only a risk is raised here. An issue, an assumption or a dependency is a person's call on the RAID Log page or over the REST API, and so are changing, moving and deleting items. - Requires ability: `raid:create` - Runs the same action as [`POST /v1/projects/{project}/raid-items`](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/add-a-raid-item) - Fires events: [`raid_item.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-created) ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose log the risk joins. | | `title` | string | yes | What the risk is called, 3 to 200 characters. | | `description` | string | no | What may happen and why it would hurt, as HTML. | | `category` | `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality`, `other` | no | The area of the project it touches; other by default. | | `probability` | integer | no | How likely, from one (rare) to five (almost certain). | | `impact` | integer | no | How bad, from one (negligible) to five (severe). | | `mitigation` | string | no | What makes it less likely or less bad, as HTML. | | `contingency` | string | no | What happens if it occurs, as HTML. | ## Example call ```json { "name": "raise_risk", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>" } } ``` ## What it returns ```json { "data": { "id": "f7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b02", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 4, "reference": "CRM-RSK-4", "type": "risk", "title": "Payment provider sandbox may lag behind production", "description": "<p>The provider ships new card flows to production weeks before its sandbox.</p>", "category": "external", "probability": 3, "impact": 4, "score": 12, "mitigation": "<p>Run the checkout smoke test against the provider's production account every Monday.</p>", "contingency": null, "owner_id": null, "owner_name": null, "raised_by": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "raised_by_name": "Sithara Perera", "due_on": null, "review_on": null, "depends_on": null, "status": "open", "position": 3, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-10-28T09:20:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — title is missing or longer than 200 characters, a rating is outside one to five, or category is not one of its values. - `FORBIDDEN` — the person's role on the project does not carry raid:create, or the token is a client guest's. - `TOKEN_MISSING_ABILITY` — token lacks raid:create. ## Example prompts > "The checkout tests only run against the provider's sandbox; raise that as a risk, likely and major, with a weekly production smoke test as the mitigation." > "Raise a risk on project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`: the only developer who knows the legacy importer leaves in December." > "Nothing on the log covers the data migration; raise a risk for it and rate it." ## Related - [list_raid_items](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-raid-items) — read the log before you raise a risk it already holds. - [get_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-raid-item) - [RAID Log API](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log) --- # record_decision_draft Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/record-decision-draft Record a decision in a project as proposed, numbered from the decisions' sequence, with its context, the options weighed and what is proposed, for a person to accept or reject. Add a decision to a project as proposed: `title` (required), `kind` (`architecture` by default), `context` (the situation and the forces at play), `options` (rows of `title`, `pros` and `cons`), `decision` (what is proposed) and `consequences` (what follows). The decision takes the next number of the decisions' sequence (`CRM-DEC-5`), starts proposed, writes its first version and goes out as `decision.created`. Record a draft when a conversation, a story or a design settles a choice that nothing records yet; give the context, at least two options with their trade-offs and the consequences, and leave the acceptance to a person. Only a proposal is recorded here. Accepting, rejecting, changing and deleting decisions, and naming the deciders and the decision it supersedes, stay with people on the Decisions page or over the REST API. - Requires ability: `decision:create` - Runs the same action as [`POST /v1/projects/{project}/decisions`](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/add-a-decision) - Fires events: [`decision.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-created) ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project the decision belongs to. | | `title` | string | yes | What the decision is called, 3 to 200 characters. | | `kind` | `architecture`, `product`, `process`, `vendor`, `security` | no | The kind; architecture by default. | | `context` | string | no | The situation and the forces at play, as HTML. | | `options` | array of object | no | The options weighed, up to ten rows, each with a `title` (required), its `pros` and its `cons`. | | `options[].title` | string | yes | What the option is, up to 200 characters. | | `options[].pros` | string | no | What speaks for it, up to 1,000 characters. | | `options[].cons` | string | no | What speaks against it, up to 1,000 characters. | | `decision` | string | no | What is proposed, as HTML. | | `consequences` | string | no | What follows if it is accepted, as HTML. | ## Example call ```json { "name": "record_decision_draft", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>" } } ``` ## What it returns ```json { "data": { "id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 5, "reference": "CRM-DEC-5", "kind": "architecture", "title": "Queue the digests through Redis", "context": "<p>The digests must not block the web requests.</p>", "options": [ { "title": "Redis queues", "pros": "Already in the stack", "cons": null }, { "title": "A managed queue service", "pros": null, "cons": "Another vendor" } ], "decision": "<p>Redis queues, one worker per project.</p>", "consequences": "<p>Each project gets its own queue worker.</p>", "status": "proposed", "decided_on": null, "deciders": [], "supersedes_id": null, "supersedes_reference": null, "position": 4, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-10-28T09:20:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `VALIDATION_FAILED` — title is missing or longer than 200 characters, an option lacks its title, or kind is not one of its values. - `FORBIDDEN` — the person's role on the project does not carry decision:create, or the token is a client guest's. - `TOKEN_MISSING_ABILITY` — token lacks decision:create. ## Example prompts > "We agreed in the stand-up to queue the digests through Redis; record that as an architecture decision with the two options we discussed." > "Record a vendor decision on project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`: send transactional mail through Resend rather than SES, with the trade-offs." > "The design review settled on Flux for the admin screens; draft the decision for the lead to accept." ## Related - [list_decisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-decisions) — read the decisions before you record one they already hold. - [get_decision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-decision) - [Decisions API](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions) --- # remove_link Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/remove-link Removes a link between two artefacts after the person confirms; both artefacts stay. Remove a link by its id, the `link_id` argument, as `list_links` lists them. Both artefacts stay; only the join goes, and with it the line in the traceability matrix. The removal goes through the same action the links panel runs and `link.deleted` goes out. The tool is destructive: a link is traceability, and losing one quietly would hide why a story exists, so a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. A staff token with `link:delete` on the project is needed. - Requires ability: `link:delete` - Runs the same action as [`DELETE /v1/links/{link}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/links/unlink-two-artefacts) - Fires events: [`link.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `link_id` | string | yes | UUID of the link to remove. | ## Example call ```json { "name": "remove_link", "arguments": { "link_id": "180a380f-c0c8-4980-a79c-2be4a78cb4d3" } } ``` ## What it returns ```json { "data": { "link_id": "e1f2a3b4-c5d6-4e7f-8a8b-9c0d1e2f3a4b", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown link_id, or a link outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry link:delete. - `TOKEN_MISSING_ABILITY` — token lacks link:delete. ## Example prompts > "Unlink SCL-8 from SCL-FR-12; it implements SCL-FR-14 instead." > "Remove the duplicate link between the pickup story and the pickup rule." ## Related - [link_artefacts](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/link-artefacts) — add a link. - [list_links](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-links) — the links of an artefact, with their ids. - [Links API](https://alm-docs.labs.zuselab.dev/api/v1/reference/links) — the same door over REST. --- # restore_revision Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/restore-revision Put an earlier version of an artefact back, which writes a new version that names the one restored and announces it. Put an earlier version of a revisable artefact back onto the record. The version's captured columns, the status included, are written onto the artefact and a new version is written with the reason `restore`, pointing at the one put back; nothing earlier is rewritten, and the artefact's family announces it (`epic.restored` for an epic). Destructive in the MCP sense: the record as it stands is replaced, so read the version with `get_revision` and confirm with the person you act for before calling. Needs `activity:read` and the artefact's own update ability (`epic:update` for an epic). A version identical to the current record is refused, since there is nothing to restore. - Requires ability: `activity:read` - Runs the same action as [`POST /v1/baselines/{baseline}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/restore-a-version-of-a-baseline) - Runs the same action as [`POST /v1/business-rules/{rule}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/restore-a-version-of-a-business-rule) - Runs the same action as [`POST /v1/change-requests/{changeRequest}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/restore-a-version-of-a-change-request) - Runs the same action as [`POST /v1/data-entities/{entity}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities/restore-a-version-of-a-entity) - Runs the same action as [`POST /v1/decisions/{decision}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/restore-a-version-of-a-decision) - Runs the same action as [`POST /v1/epics/{epic}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/restore-a-version-of-an-epic) - Runs the same action as [`POST /v1/glossary/{term}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary/restore-a-version-of-a-term) - Runs the same action as [`POST /v1/personas/{persona}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas/restore-a-version-of-a-persona) - Runs the same action as [`POST /v1/raid-items/{raidItem}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/restore-a-version-of-a-raid-item) - Runs the same action as [`POST /v1/requirements/{requirement}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/restore-a-version-of-a-requirement) - Runs the same action as [`POST /v1/stakeholders/{stakeholder}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders/restore-a-version-of-a-stakeholder) - Runs the same action as [`POST /v1/use-cases/{useCase}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/restore-a-version-of-a-use-case) - Runs the same action as [`POST /v1/work-items/{item}/revisions/{revision}/restore`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/restore-a-version-of-a-work-item) - Fires events: [`baseline.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-restored), [`business_rule.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-restored), [`change_request.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-restored), [`data_entity.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-restored), [`decision.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-restored), [`epic.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-restored), [`glossary_term.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-restored), [`persona.version_restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-version-restored), [`raid_item.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-restored), [`requirement.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-restored), [`stakeholder.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-restored), [`use_case.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-restored), [`work_item.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-restored) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `revision_id` | string | yes | UUID of the version to put back; read it with get_revision first. | ## Example call ```json { "name": "restore_revision", "arguments": { "revision_id": "e07da6e0-b4e0-4000-ae4a-df59c7fd1a20" } } ``` ## What it returns ```json { "data": { "id": "f9a0b1c2-d3e4-4f5a-8b6c-7d8e9f0a1b2c", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "revisable_type": "epic", "revisable_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "version": 4, "reason": "restore", "actor_kind": "mcp", "author_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "author_name": "Nimal Fernando", "ai_feature": null, "restored_from_id": "d6e7f8a9-b0c1-4d2e-9f3a-4b5c6d7e8f90", "restored_from_version": 1, "summary": "Restored version 1", "changed_fields": ["priority", "due_on"], "diff": { "priority": { "from": "must", "to": "should" }, "due_on": { "from": "2027-03-31", "to": null } }, "snapshot": { "title": "Loyalty tiers", "summary": null, "goal": "Reward the top fifth of members so they keep buying from the client.", "business_value": null, "hypothesis": null, "success_metrics": null, "owner_id": null, "priority": "should", "status": "draft", "starts_on": null, "due_on": null, "color": null }, "created_at": "2026-11-06T10:00:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown revision_id, a version of an artefact outside the token's scope or the person's visibility, or any version read through a guest token. - `TOKEN_MISSING_ABILITY` — token lacks activity:read or the artefact's update ability, such as epic:update. - `VALIDATION_FAILED` — the version is identical to the current record. - `FORBIDDEN` — the token holder's role on the project does not carry the artefact's update permission. ## Example prompts > "Put the loyalty tiers epic back to how it was before the priority changed." > "Restore revision `d6e7f8a9-b0c1-4d2e-9f3a-4b5c6d7e8f90` of CRM-12; I have read it and that is the one." > "Undo Heimdall's rewrite of the goal on the checkout epic." ## Related - [get_revision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-revision) — read it first. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) --- # review_quality Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/review-quality Asks Heimdall to review one work item, requirement or business rule and keeps the review, with its score, band, summary and findings; spends the team's AI budget. Ask Heimdall for a fresh quality review of one record and keep it on the record. A story is read against INVEST, its acceptance criteria, the project's business rules and its sibling stories; a requirement for one testable statement, a rationale, a measure and a fitting verification method; a business rule for its condition, action, exceptions, worked examples and authority, and for conflicts and overlaps with the project's other rules. The answer carries a score from 0 to 100 (100 less 25 per `high`, 10 per `medium` and 4 per `low` finding), its band (`ready` at 80 and above, `needs_work` from 50, `not_ready` below), a summary and the findings, each with a code, a severity, the words concerned and a suggestion in the record's own words. Nothing on the record changes: the findings are for a person to act on. The record is named by `artefact_type` (`work-item`, `requirement` or `business-rule`) and `artefact_id`. The call spends the team's monthly AI budget, so read the latest review with `list_quality_reviews` first and ask again only when the record changed or was never reviewed, never in a loop. It needs `ai:use`, a staff token and the project's assists on. - Requires ability: `ai:use` - Runs the same action as [`POST /v1/business-rules/{rule}/quality-reviews`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/review-a-business-rules-quality) - Runs the same action as [`POST /v1/requirements/{requirement}/quality-reviews`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/review-a-requirements-quality) - Runs the same action as [`POST /v1/work-items/{item}/quality-reviews`](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items/review-a-work-items-quality) - Fires events: [`business_rule.quality_reviewed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-quality-reviewed), [`requirement.quality_reviewed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-quality-reviewed), [`work_item.quality_reviewed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-quality-reviewed) ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `work-item`, `requirement`, `business-rule` | yes | The record's type: a work item, a requirement or a business rule. | | `artefact_id` | string | yes | UUID of the record to review. | ## Example call ```json { "name": "review_quality", "arguments": { "artefact_type": "work-item", "artefact_id": "5683b5a5-ee5e-4400-a374-b33e4e067dc7" } } ``` ## What it returns ```json { "data": { "id": "b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "artefact_type": "work-item", "artefact_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "version": 3, "score": 71, "band": "needs_work", "summary": "A clear story with a measurable goal; one criterion cannot be verified as written and one word leaves the scope open.", "findings": [ { "code": "testable", "severity": "high", "message": "The criterion \"the basket updates quickly\" cannot pass or fail: \"quickly\" has no limit.", "suggestion": "Then the basket total updates within 1 second of the voucher being applied.", "field": "criteria" }, { "code": "ambiguous", "severity": "low", "message": "\"Some vouchers\" leaves open which vouchers apply.", "suggestion": "Name the voucher kinds: percentage, fixed amount and free delivery.", "field": "description" } ], "findings_count": 2, "provider": "anthropic", "model": "the model the quality role is configured with", "usage_id": "c9d0e1f2-a3b4-4c5d-8e6f-7a8b9c0d1e23", "reviewed_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "reviewer_name": "Jane Doe", "reviewed_at": "2026-10-28T11:00:00+00:00", "created_at": "2026-10-28T11:00:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — artefact_type is not work-item, requirement or business-rule (the response lists them), or Heimdall could not reach its model; nothing was filed or written, and a retry a moment later is safe. - `RESOURCE_NOT_FOUND` — unknown artefact_id, or a record of a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, the holder's role does not carry ai:use, or the project's assists are off. - `AI_BUDGET_EXHAUSTED` — the team's monthly AI budget has no room; a team admin raises it under Settings → AI. - `TOKEN_MISSING_ABILITY` — token lacks ai:use. ## Example prompts > "Review CRM-142 for quality before I pick it up, and tell me what would block a developer." > "Run a quality review on requirement `3a4b5c6d-7e8f-4a9b-8c0d-1e2f3a4b5c6d` and list the findings by severity." > "Review business rule CRM-BR-7 and tell me whether it overlaps with any other rule of the project." ## Related - [list_quality_reviews](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-quality-reviews) — read the latest review before paying for another. - [get_work_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/build/get-work-item) — the story and its criteria the review is about. - [comment_on_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/comment-on-item) — record what you did about a finding. - [Work items API](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) — the same review under each record's quality reviews endpoint. - [requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements) — the same review under each record's quality reviews endpoint. - [business rules API](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules) — the same review under each record's quality reviews endpoint. --- # start_brief_import Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-brief-import Hands a whole brief to Heimdall's reader, so a document pasted into the conversation lands on the review queue as a scaffold of drafts. Start a brief import on a project: `project_id`, `title` and `text` (the brief as plain text or Markdown, 40 to 60,000 characters) are required; `source` is `pasted_text` by default or `meeting_notes`. Heimdall reads the brief on the queue and drafts epics, stories, requirements, rules, glossary terms, personas, use cases, risks, decisions and stakeholders into the review queue; nothing reaches the record until a person accepts a row. The batch is written through the same action the Brief Import page runs and `import.started` goes out. Use it for a document; for a few lines, write the records directly with the create tools. Poll `get_import_batch` until the status is `ready` or `failed`, then read the suggestions and point the person at the queue. The call spends the team's AI budget. A staff token with `import:create` on the project is needed. - Requires ability: `import:create` - Runs the same action as [`POST /v1/projects/{project}/imports`](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports/import-a-brief) - Fires events: [`import.started`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-started) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project the brief is about. | | `title` | string | yes | What the batch is called on the list, up to 160 characters. | | `text` | string | yes | The brief, as plain text or Markdown, 40 to 60,000 characters. | | `source` | `pasted_text`, `meeting_notes` | no | Where the words came from; pasted_text by default. | ## Example call ```json { "name": "start_brief_import", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>", "text": "<text>" } } ``` ## What it returns ```json { "data": { "id": "f2a3b4c5-d6e7-4f8a-8b9c-0d1e2f3a4b5c", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "source": "pasted_text", "status": "queued", "title": "Laundry shop brief", "excerpt": "An informational website for a laundry shop with order placement, tracking, payment and appointment scheduling…", "suggestion_count": 0, "provider": null, "model": null, "usage_id": null, "failure_reason": null, "created_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "creator_name": "Bea Analyst", "started_at": null, "finished_at": null, "created_at": "2026-10-28T11:20:00+00:00", "updated_at": "2026-10-28T11:20:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — title or text missing, a text shorter than 40 or longer than 60,000 characters, or a source outside the two. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, the holder's role on the project does not carry import:create, or the project's assists are off. - `TOKEN_MISSING_ABILITY` — token lacks import:create. ## Example prompts > "Here is the client's four-page brief; import it and draft the record from it." > "Read these meeting notes into the laundry project." > "Import this requirements document and tell me when the drafts are ready." ## Related - [get_import_batch](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-import-batch) — the reading and its suggestions. - [decide_suggestion](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/decide-suggestion) — a row decided on the person's word. - [convene_council](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — the council on the batch once it is ready. - [Imports API](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports) — the same door over REST. --- # start_project_research Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/start-project-research Sends Heimdall to research a project's domain, so a conversation that opened a project can ask for the industry's expectations without leaving the panel. Start a research sweep on a project by `project_id`. Heimdall reads the project's brief and record, searches the web for what products of this kind are expected to do, the regulations and compliance they meet, the integrations clients ask for and the accessibility and security baselines, and files what it found as research notes and as suggestions on the review queue. The run is queued through the same action the project dashboard runs and `research.started` goes out; one run at a time per project. Read `list_research_notes` once the status is `ready`. The call spends the team's AI budget. A staff token with `import:create` on the project is needed. - Requires ability: `import:create` - Runs the same action as [`POST /v1/projects/{project}/research`](https://alm-docs.labs.zuselab.dev/api/v1/reference/research/research-a-projects-domain) - Fires events: [`research.started`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-started) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project to research. | ## Example call ```json { "name": "start_project_research", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": { "id": "c5d6e7f8-a9b0-4c1d-8e2f-3a4b5c6d7e8f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "status": "queued", "note_count": 0, "suggestion_count": 0, "provider": null, "model": null, "usage_id": null, "failure_reason": null, "created_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "creator_name": "Bea Analyst", "started_at": null, "finished_at": null, "created_at": "2026-10-28T11:50:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a sweep is already queued or running on the project. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, the holder's role on the project does not carry import:create, or the project's assists are off. - `TOKEN_MISSING_ABILITY` — token lacks import:create. ## Example prompts > "Research what laundry ordering apps usually offer and what regulations apply to card payments here." > "Run the research sweep on the laundry project." ## Related - [list_research_notes](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-research-notes) — what the sweep found. - [get_import_batch](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-import-batch) — the suggestions the sweep filed sit with the imports. - [Research API](https://alm-docs.labs.zuselab.dev/api/v1/reference/research) — the same door over REST. --- # submit_for_review Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review Puts an epic, story, requirement, business rule, use case, baseline or change request up for approval under the record's own submit ability, or resubmits it as the next round when its open request waits for changes. Submit a record for approval once it is ready. `artefact_type` names the family (`epic`, `work-item` for a story, `requirement`, `business-rule`, `use-case`, `baseline` or `change-request`) and `artefact_id` the record; `summary` tells the approvers what changed and why. The record moves into review where it has a review status, the approvers are resolved from the project's policy, each is notified, and `approval.requested` goes out; a project whose gate is off approves it at once. When the record's open request is waiting for changes, the same call resubmits it as the next round. A baseline passes two gates through this one door: a composed draft goes to the project manager's **Baseline approval**, which freezes it and locks the records it holds, and a frozen scope baseline goes, on the next call, to the **Client sign-off** of the stakeholders flagged as approving requirements. A change request goes to the **Change request** gate with the stakeholder who asked for it added to the approvers; its approval unlocks the baseline items it names. The tool needs the record's own ability: the update ability for a planning record (`requirement:update` for a requirement, and so on), `baseline:freeze` for a baseline, `change-request:approve` for a change request, and a staff token. Deciding is a person's act: there is no tool for it. Read the request afterwards with `get_approval_request`. - Runs the same action as [`POST /v1/baselines/{baseline}/submit`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-baseline) - Runs the same action as [`POST /v1/change-requests/{changeRequest}/submit`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-change-request) - Runs the same action as [`POST /v1/business-rules/{rule}/submit`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-business-rule) - Runs the same action as [`POST /v1/requirements/{requirement}/submit`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-requirement) - Runs the same action as [`POST /v1/use-cases/{useCase}/submit`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-use-case) - Runs the same action as [`POST /v1/epics/{epic}/submit`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-an-epic) - Runs the same action as [`POST /v1/work-items/{item}/submit`](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals/submit-a-story) - Fires events: [`approval.requested`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-requested) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `baseline`, `change-request`, `generated-document`, `sprint` | yes | The family of the record to submit. | | `artefact_id` | string | yes | UUID of the record to submit. | | `summary` | string | no | What changed and why, for the approvers; up to 2000 characters. | ## Example call ```json { "name": "submit_for_review", "arguments": { "artefact_type": "epic", "artefact_id": "5683b5a5-ee5e-4400-a374-b33e4e067dc7" } } ``` ## What it returns ```json { "data": { "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "approvable_type": "requirement", "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Apply a voucher at checkout", "kind": "requirement_review", "status": "pending", "rule": "any", "quorum": 1, "round": 1, "approvers": [ { "user_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "name": "Pat Manager", "via": "role", "stakeholder_id": null } ], "tally": { "approved": 0, "needed": 1, "total": 1 }, "requires_comment": true, "requested_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "requester_name": "Bea Analyst", "summary": "First cut of the voucher rules.", "revision_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "due_at": "2026-10-31T09:20:00+00:00", "remind_at": "2026-10-29T09:20:00+00:00", "auto_approve_at": null, "reminded_at": null, "escalated_at": null, "decided_at": null, "auto_approved": false, "decisions": [], "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-10-28T09:20:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — unknown artefact_type; the record is already under review and not waiting for changes; it cannot be submitted as it stands (a task, a done story, an empty baseline, a signed baseline, an approved change request); the gate names nobody (a scope baseline with no stakeholder seat flagged and linked); summary is longer than 2000 characters. - `RESOURCE_NOT_FOUND` — unknown artefact_id, or a record outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry the record's submit permission. - `TOKEN_MISSING_ABILITY` — token lacks the record's submit ability. ## Example prompts > "The story CRM-142 is complete; submit it for review and say I added the negative criteria." > "Submit requirement `a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13` for approval with a note that the measure was tightened." > "Submit the v1.0 scope baseline to Pat for approval, and once it is frozen send it to the client to sign." > "The impact of CRM-CR-2 is written; put it to the approvers." ## Related - [get_approval_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-approval-request) — the request with its verdicts. - [list_pending_approvals](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-pending-approvals) — the inbox. - [review_quality](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/review-quality) — the reading to run before you submit a planning record. - [analyse_change_impact](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/analyse-change-impact) — the reading to run before you submit a change request. - [Approvals API](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals) — the same door over REST, per family. --- # sweep_improvements Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/sweep-improvements Runs Heimdall's improvement sweep on a project and answers with how many gaps became cards, how many cards came down and how many wait; calls no model and spends nothing. Run Heimdall's improvement sweep on one project now and read what it did. The sweep reads the record for five structural gaps (a non-functional requirement without a measure, a story without a negative acceptance criterion, a business rule without a worked example, a data entity without an owner, an open risk without a mitigation) and leaves one improvement card per new gap on the review queue, titled with the record's reference; a card a person kept or dismissed is never proposed again, and a pending card whose gap was filled since is taken down. It calls no model and spends nothing. Read the cards with the project's suggestions over REST; deciding them stays with people. The project is named by `project_id`. The same sweep runs weekly on every live project and when a brief import is fully decided, so running it is useful right after records were written. It needs `import:create`, a staff token and the project's assists on. - Requires ability: `import:create` - Runs the same action as [`POST /v1/projects/{project}/improvements`](https://alm-docs.labs.zuselab.dev/api/v1/reference/improvements/sweep-a-projects-record) - Fires events: [`improvement.swept`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/improvement/improvement-swept) ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project to sweep. | ## Example call ```json { "name": "sweep_improvements", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": { "proposed": 3, "retired": 1, "pending": 5 } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, the holder's role on the project does not carry import:create, or the project's assists are off. - `TOKEN_MISSING_ABILITY` — token lacks import:create. ## Example prompts > "Sweep the Customer Portal's record for gaps and tell me how many cards are waiting." > "I just drafted the requirements for CRM; run the improvement sweep and summarise what it found." > "Which checks does the sweep run, and how many cards did it leave on project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`?" ## Related - [review_quality](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/review-quality) — the metered reading of one record, for the judgement calls the sweep leaves alone. - [convene_council](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — several lenses on one record. - [Improvements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/improvements) — the same sweep over REST. - [Research API](https://alm-docs.labs.zuselab.dev/api/v1/reference/research) — the suggestions endpoint that lists the cards. --- # update_approval_policy Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-approval-policy Writes a project's approval policy for one kind, after the person confirms; who decides, how many must agree and how long they have. Write a project's approval policy: `project_id` and `kind` (`requirement_review`, the kind open today) are required, with any of `rule` (`any`, `all` or `quorum`), `quorum` (1 to 50), `approver_roles` (project roles whose holders approve; the guest role never), `approver_user_ids` (members of the team asked by name), `approver_stakeholder_ids` (the client's seats, deciding once linked to an account), `sla_hours`, `reminder_hours` and `auto_approve_after_hours` (1 to 720, null for no clock), `requires_comment_on_reject` and `enabled`. A key left out takes the workspace default, not the project's current value, so send the whole policy as it should stand. Requests already open keep the approvers they were sent to. The policy goes through the same action the Settings page runs and `approval.policy_updated` goes out with what changed. The tool is destructive in the MCP sense: a policy decides whose word carries a record, so a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. A staff token with `approval:manage-policies` on the project is needed. - Requires ability: `approval:manage-policies` - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `kind` | `requirement_review`, `client_sign_off`, `baseline_approval`, `change_request`, `document_approval`, `sprint_commitment` | yes | The kind of approval the policy decides. | | `rule` | `any`, `all`, `quorum` | no | How many approvals carry a request. | | `quorum` | integer | no | How many approvals carry a request under the quorum rule, 1 to 50. | | `approver_roles` | array of `owner`, `project_manager`, `business_analyst`, `developer`, `qa_engineer`, `release_manager`, `viewer`, `guest` | no | The project roles whose holders approve. | | `approver_user_ids` | array of string | no | UUIDs of the team members asked by name. | | `approver_stakeholder_ids` | array of string | no | UUIDs of the client's stakeholder seats asked. | | `sla_hours` | integer | no | Hours to decide before the request is overdue, 1 to 720; null for no clock. | | `reminder_hours` | integer | no | Hours after which the undecided approvers are nudged once; null for none. | | `auto_approve_after_hours` | integer | no | Hours after which an unanswered request carries by itself; null to never let it. | | `requires_comment_on_reject` | boolean | no | Whether a rejection or a request for changes must say why. | | `enabled` | boolean | no | Whether the gate is on; off, a submission is approved at once. | ## Example call ```json { "name": "update_approval_policy", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "kind": "requirement_review" } } ``` ## What it returns ```json { "data": { "kind": "requirement_review", "rule": "all", "quorum": 1, "approver_roles": ["project_manager", "business_analyst"], "approver_user_ids": [], "approver_stakeholder_ids": ["5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b"], "sla_hours": 48, "reminder_hours": 24, "auto_approve_after_hours": null, "requires_comment_on_reject": true, "enabled": true, "stored": true } } ``` ## How it fails - `VALIDATION_FAILED` — a kind that is not open, a rule outside the three, a quorum or a clock outside its range, more roles, people or seats than allowed, the guest role, a person who is not on the team, a seat that is not the project's, or a gate switched on with nobody named. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry approval:manage-policies. - `TOKEN_MISSING_ABILITY` — token lacks approval:manage-policies. ## Example prompts > "Make the client sign off requirements on the laundry project too, and give everyone two days." > "Switch the requirement review off on the internal project." > "Require every approver to agree, not just one." ## Related - [list_pending_approvals](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-pending-approvals) — the requests the policy shapes. - [create_stakeholder](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/people/create-stakeholder) — a seat the policy may name. - [Approvals API](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals) — the policies over REST. --- # update_baseline Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-baseline Changes a draft baseline's name, kind, words or records, or moves a frozen baseline to superseded; a frozen baseline's fields and records never change, and the approval engine's moves are refused. Change a draft, or retire a frozen baseline. For a draft, send any of `name`, `kind` and `description`, and `items` to replace the record rows wholesale (the same objects of `type` and `id` as `create_baseline`; an empty list empties the draft); only approved records join. For a frozen baseline the one change allowed is `status: superseded`, which keeps the baseline readable with every stamp and stops it locking its records; the moves through in review, approved, awaiting sign-off and signed off belong to the approval engine and are refused. Runs the same actions as the application's editor and **Move to** menu: `baseline.updated` with what changed, `baseline.status_changed` for a supersede, and a version per save. - Requires ability: `baseline:update` - Runs the same action as [`PATCH /v1/baselines/{baseline}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/update-a-baseline) - Runs the same action as [`PUT /v1/baselines/{baseline}/items`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/set-a-baselines-items) - Runs the same action as [`POST /v1/baselines/{baseline}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines/move-a-baseline) - Fires events: [`baseline.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-updated), [`baseline.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-status-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `baseline_id` | string | yes | UUID of the baseline. | | `name` | string | no | What the baseline is called, 3 to 120 characters. | | `kind` | `scope`, `release`, `sprint`, `milestone` | no | The kind of baseline. | | `description` | string | no | What the baseline marks, as HTML. | | `items` | array of any | no | The approved records it holds, replacing the list: objects with type and id. | | `status` | `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off`, `superseded` | no | Superseded, when a newer baseline replaces this one; the other states are the approval engine's. | ## Example call ```json { "name": "update_baseline", "arguments": { "baseline_id": "a7a5d36a-225a-4800-a992-aad35d1bd7c6" } } ``` ## What it returns ```json { "data": { "id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "v1.1 scope (pilot)", "kind": "milestone", "description": "<p>The pilot cut of the second release.</p>", "status": "draft", "frozen": false, "locks_items": false, "version": 0, "approved_at": null, "approved_revision_id": null, "frozen_at": null, "frozen_by": null, "signed_off_at": null, "items": [ { "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e", "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "artefact_type": "requirement", "artefact_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Password reset by email", "version": 2, "revision_id": null, "frozen_at": null, "locked": false, "unlocked_at": null, "unlocked_by_change_request_id": null, "position": 0 } ], "position": 1, "created_at": "2026-10-29T09:00:00+00:00", "updated_at": "2026-10-29T09:30:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field or record change on a baseline past its draft; a status other than superseded, or superseded from a status that does not allow it; a name shorter than 3 or longer than 120 characters or already used; an unknown kind; an item that cannot be baselined or is not approved. - `RESOURCE_NOT_FOUND` — unknown baseline_id, a baseline outside the token's scope, or an item naming a record that is hidden, gone or another project's. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry baseline:update. - `TOKEN_MISSING_ABILITY` — token lacks baseline:update. ## Example prompts > "Rename the draft baseline Release 1.1 to Milestone: pilot and make it a milestone baseline." > "Replace the records of baseline `6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d` with the approved requirements of the loyalty epic." > "The v1.0 scope is replaced by v1.1; mark the old one superseded." ## Related - [create_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-baseline) — compose a new draft. - [delete_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-baseline) — remove a draft that was never frozen. - [get_baseline](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-baseline) — read the result with its records. - [Baselines API](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines) — the same writes over REST, the records through their own endpoint. --- # update_business_rule Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-business-rule Changes a business rule in part and moves it where the lifecycle allows by hand; the review states stay with the approvers. Change a business rule by `business_rule_id` with any of `name`, `type` (`constraint`, `derivation`, `computation`, `inference`, `action_enabler` or `policy`), `statement` (HTML), `condition`, `action`, `exceptions` (HTML), `examples` (replaces the rows of `input` and `expected`), `authority_stakeholder_id`, `authority`, `priority`, `effective_from`, `effective_to`, `owner_id` and `status`. A field left out keeps its value; a field sent as null is cleared. The fields go through the same action the Business Rules page runs and write a version; `business_rule.updated` goes out. A `status` goes through its own move: `deprecated`, `baselined` or back to `draft` are moves a person may make by hand, and `business_rule.status_changed` goes out. `in_review` and `approved` are reached through `submit_for_review` and the approvers, and any move is refused while an approval request is open. A staff token with `business-rule:update` on the project is needed. - Requires ability: `business-rule:update` - Runs the same action as [`PATCH /v1/business-rules/{rule}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/update-a-business-rule) - Runs the same action as [`POST /v1/business-rules/{rule}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules/move-a-business-rule) - Fires events: [`business_rule.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-updated), [`business_rule.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-status-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `business_rule_id` | string | yes | UUID of the business rule. | | `name` | string | no | What the rule is called, 3 to 200 characters. | | `type` | `constraint`, `derivation`, `computation`, `inference`, `action_enabler`, `policy` | no | The kind of rule. | | `statement` | string | no | The rule in plain words, as HTML. | | `condition` | string | no | When the rule applies. | | `action` | string | no | What follows when it does. | | `exceptions` | string | no | When it does not apply, as HTML. | | `examples` | array of any | no | Worked examples replacing the list: objects with input and expected. | | `authority_stakeholder_id` | string | no | UUID of the stakeholder who may change the rule. | | `authority` | string | no | Who or what the rule's authority is, in words. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority. | | `effective_from` | string | no | When the rule comes into force, YYYY-MM-DD. | | `effective_to` | string | no | When it stops applying, YYYY-MM-DD. | | `owner_id` | string | no | UUID of the person who answers for it. | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | no | A move the lifecycle allows by hand; the review states go through submit_for_review. | ## Example call ```json { "name": "update_business_rule", "arguments": { "business_rule_id": "25b88aea-9286-4600-a447-00cad0062444" } } ``` ## What it returns ```json { "data": { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 4, "reference": "SCL-BR-4", "name": "A pickup needs a paid order", "type": "constraint", "statement": "<p>A pickup slot can be booked only against an order that is paid in full.</p>", "condition": "When a pickup is booked", "action": "the order must be paid in full", "exceptions": null, "examples": [ { "input": "Unpaid order, pickup requested", "expected": "Refused with a payment link" } ], "authority_stakeholder_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b", "authority_stakeholder_name": "Nadeesha Perera", "authority": "The shop's credit policy", "priority": "must", "effective_from": null, "effective_to": null, "status": "draft", "owner_id": null, "owner_name": null, "position": 4, "version": 2, "approved_at": null, "approved_revision_id": null, "created_at": "2026-10-28T10:40:00+00:00", "updated_at": "2026-10-28T12:00:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a type or priority outside its set, an example without an input, a status of in_review or approved (the message says to submit the rule for review), a move the lifecycle does not allow, a move while a request is open, or a stakeholder or owner that is not the project's or the team's. - `RESOURCE_NOT_FOUND` — unknown business_rule_id, or a rule outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry business-rule:update. - `TOKEN_MISSING_ABILITY` — token lacks business-rule:update. ## Example prompts > "Add a worked example to the pickup rule: an unpaid order asking for a pickup is refused with a payment link." > "Make Nadeesha the authority on SCL-BR-4." > "Deprecate the cash-on-delivery rule." ## Related - [get_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-business-rule) — the rule as it stands. - [propose_business_rule](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/propose-business-rule) — a new one. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — the road into review and approved. - [Business rules API](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules) — the same doors over REST. --- # update_change_request Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-change-request Changes a change request's words, records touched or impact while it is not yet decided, or moves it by hand along its lifecycle; the moves through the approval gate are refused. Change a request until the approvers decide it, and move it along afterwards. Send any of the fields `raise_change_request` takes; `affected` replaces the records touched wholesale. A request is edited while `submitted`, `under_analysis` or `awaiting_approval`; an approved or rejected one no longer changes. `status` makes a move by hand: `under_analysis` (needs `change-request:analyse` on the project), `closed`, and after the approval `implementing`, `implemented`, `verified` (needs `change-request:verify`, and stamps who verified and when) and `closed`. The moves into and out of the gate (`awaiting_approval`, `approved`, `rejected`) are the approval engine's and are refused; use `submit_for_review` on the person's word. Runs the same actions as the application's editor and **Move to** menu: `change_request.updated` with what changed, the records touched included, `change_request.status_changed` for a move, and a version per save. - Requires ability: `change-request:update` - Runs the same action as [`PATCH /v1/change-requests/{changeRequest}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/update-a-change-request) - Runs the same action as [`POST /v1/change-requests/{changeRequest}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/move-a-change-request) - Runs the same action as [`POST /v1/change-requests/{changeRequest}/verify`](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests/verify-a-change-request) - Fires events: [`change_request.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-updated), [`change_request.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-status-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `change_request_id` | string | yes | UUID of the change request. | | `title` | string | no | What the change is called, 3 to 200 characters. | | `description` | string | no | What is asked for and why, as HTML. | | `reason` | `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt`, `scope_reduction` | no | Why it was raised. | | `baseline_id` | string | no | UUID of the baseline the change is raised against. | | `stakeholder_id` | string | no | UUID of the stakeholder who asked. | | `affected` | array of any | no | The records it touches, replacing the list: objects with type and id. | | `impact_scope` | string | no | What the change touches, as HTML. | | `impact_schedule_days` | integer | no | Days the schedule moves; negative when it shortens. | | `impact_effort_hours` | number | no | Hours of effort. | | `impact_cost` | number | no | The cost, in the project's currency. | | `impact_risk` | string | no | What the change risks, as HTML. | | `impact_level` | `low`, `medium`, `high` | no | How hard it hits the project. | | `priority` | `lowest`, `low`, `medium`, `high`, `highest` | no | How urgent the change is. | | `status` | `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified`, `closed` | no | A move by hand the lifecycle allows; the gate's states go through submit_for_review. | ## Example call ```json { "name": "update_change_request", "arguments": { "change_request_id": "d2589e11-c5a2-4000-ac6f-e7a8e29873a7" } } ``` ## What it returns ```json { "data": { "id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 2, "reference": "CRM-CR-2", "title": "Accept vouchers on every renewal", "description": "<p>The client wants the voucher applied to every renewal.</p>", "reason": "client_request", "requested_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "requester_name": "Bea Analyst", "stakeholder_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "stakeholder_name": "Gail of the Client", "baseline_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "baseline_name": "v1.0 scope", "affected": [ { "artefact_type": "requirement", "artefact_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "reference": "CRM-FR-12", "title": "Password reset by email" }, { "artefact_type": "business-rule", "artefact_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "reference": "CRM-BR-7", "title": "One voucher per order" } ], "impact_scope": "<p>CRM-FR-12 gains a renewal clause; CRM-BR-7 gains an exception for renewals.</p>", "impact_schedule_days": 5, "impact_effort_hours": "24.00", "impact_cost": "1800.00", "impact_risk": "<p>The renewal job runs unattended.</p>", "impact_level": "high", "ai_impact_draft": null, "analysed_at": null, "priority": "high", "status": "under_analysis", "decision_note": null, "verified_by": null, "verified_at": null, "version": 0, "approved_at": null, "approved_revision_id": null, "position": 1, "created_at": "2026-10-29T09:20:00+00:00", "updated_at": "2026-10-29T10:10:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field change on a request already decided; a status the lifecycle does not allow from where the request stands, a status the approval engine owns, or any move while a request is under review; a title shorter than 3 or longer than 200 characters; an unknown reason, priority, impact_level or status; an impact number out of range; a baseline or stakeholder that is not the project's. - `RESOURCE_NOT_FOUND` — unknown change_request_id, a request outside the token's scope, or a record under affected that is hidden, gone or another project's. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry change-request:update (or change-request:analyse and change-request:verify for those moves). - `TOKEN_MISSING_ABILITY` — token lacks change-request:update. ## Example prompts > "Set the impact of CRM-CR-2 to high, five days and 24 hours, and add the voucher rule to the records it touches." > "Move change request `c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f` under analysis; I am looking at it now." > "The voucher change is built; mark CRM-CR-2 implemented." ## Related - [raise_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/raise-change-request) — raise a new request. - [analyse_change_impact](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/analyse-change-impact) — fill the empty impact fields from Heimdall's reading. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — the move into the gate, on the person's word. - [delete_change_request](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-change-request) — remove a request that was never agreed. - [Change Requests API](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests) — the same writes over REST, the verify move through its own endpoint. --- # update_decision Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-decision Changes a decision in part and moves it along its lifecycle when a status is sent; accepting stamps the date decided. Change a decision by `decision_id` with any of `title`, `kind` (`architecture`, `product`, `process`, `vendor` or `security`), `context` (HTML), `options` (replaces the rows of `title`, `pros` and `cons`), `decision` (what was decided, HTML), `consequences` (HTML), `deciders` (replaces the user ids), `supersedes_id` and `status` (`proposed`, `accepted`, `rejected`, `superseded` or `deprecated`; a move the lifecycle allows from where the decision stands). A field left out keeps its value; a field sent as null is cleared. The fields go through the same action the Decisions page runs and write a version; `decision.updated` goes out. A `status` goes through its own move, accepting stamps the date decided, and `decision.status_changed` goes out. A staff token with `decision:update` on the project is needed. - Requires ability: `decision:update` - Runs the same action as [`PATCH /v1/decisions/{decision}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/update-a-decision) - Runs the same action as [`POST /v1/decisions/{decision}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions/move-a-decision) - Fires events: [`decision.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-updated), [`decision.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-status-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `decision_id` | string | yes | UUID of the decision. | | `title` | string | no | What the decision is called, 3 to 200 characters. | | `kind` | `architecture`, `product`, `process`, `vendor`, `security` | no | The kind of decision. | | `context` | string | no | The situation and the forces at play, as HTML. | | `options` | array of any | no | The options weighed, replacing the list: objects with title, pros and cons. | | `decision` | string | no | What was decided, as HTML. | | `consequences` | string | no | What follows, as HTML. | | `deciders` | array of string | no | UUIDs of the people who decided; the list replaces the current one. | | `supersedes_id` | string | no | UUID of the earlier decision this one replaces. | | `status` | `proposed`, `accepted`, `rejected`, `superseded`, `deprecated` | no | Where the decision stands; a move the lifecycle allows. | ## Example call ```json { "name": "update_decision", "arguments": { "decision_id": "bdee5679-3aa7-4c00-a3d9-2fc0e79341ff" } } ``` ## What it returns ```json { "data": { "id": "d0e1f2a3-b4c5-4d6e-8f7a-8b9c0d1e2f3a", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 2, "reference": "SCL-DEC-2", "kind": "architecture", "title": "Take card payments through the house gateway", "context": "<p>The shop wants card payments at placement; two gateways serve Sri Lanka.</p>", "options": [ { "title": "House gateway", "pros": "Already integrated in three projects", "cons": "Higher fee on small orders" }, { "title": "Bank gateway", "pros": "Lower fee", "cons": "No sandbox, six-week onboarding" } ], "decision": "<p>The house gateway, with the sandbox first.</p>", "consequences": "<p>Fees are passed on above a threshold; the bank gateway is revisited at volume.</p>", "status": "accepted", "decided_on": "2026-11-03", "deciders": ["3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b"], "supersedes_id": null, "supersedes_reference": null, "position": 2, "created_at": "2026-10-28T11:10:00+00:00", "updated_at": "2026-11-03T10:00:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a kind or status outside its set, an option without a title, a decider who is not on the team, a superseded decision that is not the project's, or a move the lifecycle does not allow from where the decision stands. - `RESOURCE_NOT_FOUND` — unknown decision_id, or a decision outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry decision:update. - `TOKEN_MISSING_ABILITY` — token lacks decision:update. ## Example prompts > "Accept the gateway decision; Pat decided it today." > "Add the bank gateway as a second option with its pros and cons." > "Mark SCL-DEC-1 as superseded by SCL-DEC-2." ## Related - [get_decision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-decision) — the decision as it stands. - [record_decision_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/record-decision-draft) — a new one. - [delete_decision](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-decision) — take it away. - [Decisions API](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions) — the same doors over REST. --- # update_epic Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-epic Changes an epic in part and moves it where the lifecycle allows by hand; the review states stay with the approvers. Change an epic by `epic_id` with any of `title`, `summary`, `goal`, `business_value`, `hypothesis`, `success_metrics`, `owner_id`, `priority`, `starts_on`, `due_on`, `color` and `status`. A field left out keeps its value; a field sent as null is cleared. The fields go through the same action the Epics page runs and write a version; `epic.updated` goes out. A `status` goes through the epic's own move: `in_progress`, `done`, `cancelled` or back to `draft` are moves a person may make by hand, and `epic.status_changed` goes out. `in_review` and `approved` are reached through `submit_for_review` and the approvers, and any move is refused while an approval request is open. A staff token with `epic:update` on the project is needed. - Requires ability: `epic:update` - Runs the same action as [`PATCH /v1/epics/{epic}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/update-an-epic) - Runs the same action as [`POST /v1/epics/{epic}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics/move-an-epic) - Fires events: [`epic.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-updated), [`epic.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-status-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `epic_id` | string | yes | UUID of the epic. | | `title` | string | no | What the epic is called, 3 to 200 characters. | | `summary` | string | no | What the epic covers, as HTML. | | `goal` | string | no | One sentence on what it achieves. | | `business_value` | string | no | Why the client wants it, as HTML. | | `hypothesis` | string | no | The bet it makes, if any. | | `success_metrics` | string | no | How the team will know it worked, as HTML. | | `owner_id` | string | no | UUID of the person who answers for it. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority; should by default. | | `starts_on` | string | no | When work on it starts, YYYY-MM-DD. | | `due_on` | string | no | When it is due, YYYY-MM-DD. | | `color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` | no | The badge colour the board and the roadmap paint it in. | | `status` | `draft`, `in_review`, `approved`, `in_progress`, `done`, `cancelled` | no | A move the lifecycle allows by hand; the review states go through submit_for_review. | ## Example call ```json { "name": "update_epic", "arguments": { "epic_id": "a8ab1070-211b-4c00-a69c-69c9d0e5a810" } } ``` ## What it returns ```json { "data": { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 1, "reference": "SCL-1", "title": "Order placement", "summary": "<p>Services, items, pickup or drop-off, and the payment step.</p>", "goal": "A customer books a wash in under two minutes.", "business_value": "<p>Every order placed online is one the counter does not type in.</p>", "hypothesis": null, "success_metrics": "<p>Seventy percent of orders placed online by month three.</p>", "owner_id": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "owner_name": "Bea Analyst", "priority": "must", "status": "in_progress", "starts_on": "2026-11-02", "due_on": "2026-12-19", "color": "emerald", "position": 1, "version": 3, "approved_at": "2026-10-30T10:00:00+00:00", "approved_revision_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "created_at": "2026-10-28T09:55:00+00:00", "updated_at": "2026-11-02T08:00:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a status of in_review or approved (the message says to submit the epic for review), a move the lifecycle does not allow from where the epic stands, a move while a request is open, or an owner_id who is not on the team. - `RESOURCE_NOT_FOUND` — unknown epic_id, or an epic outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry epic:update. - `TOKEN_MISSING_ABILITY` — token lacks epic:update. ## Example prompts > "Give the order placement epic a business value and a success metric." > "Move the informational site epic into progress; the sprint started." > "Cancel the driver app epic; the client dropped it." ## Related - [get_epic](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-epic) — the epic as it stands. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — the road into review and approved. - [list_revisions](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/list-revisions) — every version the changes wrote. - [Epics API](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) — the same doors over REST. --- # update_raid_item Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-raid-item Changes a RAID item in part and moves it along its lifecycle when a status is sent. Change a RAID item by `raid_item_id` with any of `title`, `type`, `description` (HTML), `category`, `probability`, `impact`, `mitigation`, `contingency`, `owner_id`, `due_on`, `review_on`, `depends_on` and `status` (`open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred` or `accepted`; a closed item goes back to open only). A field left out keeps its value; a field sent as null is cleared. The fields go through the same action the RAID Log page runs and write a version; `raid_item.updated` goes out. A `status` goes through its own move and `raid_item.status_changed` goes out. A staff token with `raid:update` on the project is needed. - Requires ability: `raid:update` - Runs the same action as [`PATCH /v1/raid-items/{raidItem}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/update-a-raid-item) - Runs the same action as [`POST /v1/raid-items/{raidItem}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log/move-a-raid-item) - Fires events: [`raid_item.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-updated), [`raid_item.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-status-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `raid_item_id` | string | yes | UUID of the RAID item. | | `title` | string | no | What the item is called, 3 to 200 characters. | | `type` | `risk`, `assumption`, `issue`, `dependency` | no | Risk, assumption, issue or dependency; risk by default. | | `description` | string | no | What it is, as HTML. | | `category` | `technical`, `schedule`, `scope`, `resource`, `budget`, `external`, `security`, `compliance`, `quality`, `other` | no | The area of the project it touches; other by default. | | `probability` | integer | no | How likely, 1 to 5; risks only. | | `impact` | integer | no | How bad, 1 to 5; risks and issues. | | `mitigation` | string | no | What makes it less likely or less bad, as HTML; risks only. | | `contingency` | string | no | What happens if it occurs, as HTML; risks only. | | `owner_id` | string | no | UUID of the person who answers for it. | | `due_on` | string | no | When it must be resolved, YYYY-MM-DD; issues and dependencies. | | `review_on` | string | no | When it is next reviewed, YYYY-MM-DD; risks and assumptions. | | `depends_on` | string | no | What the project depends on, in words; dependencies only. | | `status` | `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred`, `accepted` | no | Where the item stands; a move the lifecycle allows. | ## Example call ```json { "name": "update_raid_item", "arguments": { "raid_item_id": "3b518d77-65d1-4e00-a5a9-90b3213d4d69" } } ``` ## What it returns ```json { "data": { "id": "c9d0e1f2-a3b4-4c5d-8e6f-7a8b9c0d1e2f", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 3, "reference": "SCL-RSK-3", "type": "issue", "title": "The gateway sandbox is not approved yet", "description": "<p>Approved on the second application; keys arrived on the 3rd.</p>", "category": "external", "probability": null, "impact": 4, "score": null, "mitigation": null, "contingency": null, "owner_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "owner_name": "Pat Manager", "raised_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "raised_by_name": "Bea Analyst", "due_on": "2026-11-15", "review_on": null, "depends_on": null, "status": "resolved", "position": 3, "created_at": "2026-10-28T11:00:00+00:00", "updated_at": "2026-11-03T09:30:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a value outside its set, a rating outside 1 to 5, a move the lifecycle does not allow from where the item stands, or an owner_id who is not on the team. - `RESOURCE_NOT_FOUND` — unknown raid_item_id, or an item outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry raid:update. - `TOKEN_MISSING_ABILITY` — token lacks raid:update. ## Example prompts > "Resolve the sandbox issue; the keys arrived." > "Raise the probability of the courier risk to four and add a mitigation." > "Mark the price-list assumption as accepted." ## Related - [get_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-raid-item) — the item as it stands. - [delete_raid_item](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-raid-item) — take it away. - [RAID log API](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log) — the same doors over REST. --- # update_requirement Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-requirement Changes a requirement in part and moves it where the lifecycle allows by hand; the kind is fixed and the review states stay with the approvers. Change a requirement by `requirement_id` with any of `title`, `category` (a non-functional requirement's quality), `statement` and `rationale` (HTML), `source_stakeholder_id`, `source` (where it came from, in words), `priority`, `measure`, `verification_method` (`test`, `inspection`, `analysis` or `demonstration`), `owner_id` and `status`. The kind, functional or non-functional, never changes: it picked the sequence the requirement was numbered from. A field left out keeps its value; a field sent as null is cleared. The fields go through the same action the Requirements page runs and write a version; `requirement.updated` goes out. A `status` goes through its own move: `deprecated`, `baselined` or back to `draft` are moves a person may make by hand, and `requirement.status_changed` goes out. `in_review` and `approved` are reached through `submit_for_review` and the approvers, and any move is refused while an approval request is open. A staff token with `requirement:update` on the project is needed. - Requires ability: `requirement:update` - Runs the same action as [`PATCH /v1/requirements/{requirement}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/update-a-requirement) - Runs the same action as [`POST /v1/requirements/{requirement}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements/move-a-requirement) - Fires events: [`requirement.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-updated), [`requirement.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-status-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `requirement_id` | string | yes | UUID of the requirement. | | `title` | string | no | What the requirement is called, 3 to 200 characters. | | `category` | `performance`, `scalability`, `availability`, `reliability`, `security`, `privacy`, `usability`, `accessibility`, `compatibility`, `maintainability`, `portability`, `compliance`, `observability`, `localisation`, `other` | no | The quality a non-functional requirement is about. | | `statement` | string | no | What the system shall do or keep, as HTML. | | `rationale` | string | no | Why, as HTML. | | `source_stakeholder_id` | string | no | UUID of the stakeholder it came from. | | `source` | string | no | Where it came from, in words. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority. | | `measure` | string | no | The metric and target that say when a non-functional requirement is met. | | `verification_method` | `test`, `inspection`, `analysis`, `demonstration` | no | How it will be shown to be met. | | `owner_id` | string | no | UUID of the person who answers for it. | | `status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | no | A move the lifecycle allows by hand; the review states go through submit_for_review. | ## Example call ```json { "name": "update_requirement", "arguments": { "requirement_id": "b0d1c030-4f2c-4000-aee6-b98971adea50" } } ``` ## What it returns ```json { "data": { "id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "type": "functional", "number": 12, "reference": "SCL-FR-12", "category": null, "title": "Orders can be paid online", "statement": "<p>The system shall take a card payment at order placement.</p>", "rationale": "<p>The counter stops chasing payment on collection.</p>", "source_stakeholder_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b", "source_stakeholder_name": "Nadeesha Perera", "source": "Kick-off call", "priority": "must", "measure": null, "verification_method": "test", "status": "draft", "owner_id": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "owner_name": "Bea Analyst", "position": 12, "version": 2, "approved_at": null, "approved_revision_id": null, "created_at": "2026-10-28T10:30:00+00:00", "updated_at": "2026-10-28T11:45:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a category, priority or verification_method outside its set, a status of in_review or approved (the message says to submit the requirement for review), a move the lifecycle does not allow, a move while a request is open, or a stakeholder or owner that is not the project's or the team's. - `RESOURCE_NOT_FOUND` — unknown requirement_id, or a requirement outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry requirement:update. - `TOKEN_MISSING_ABILITY` — token lacks requirement:update. ## Example prompts > "Make SCL-FR-12 a must and tighten the statement to a card payment at placement." > "Set the measure on the page-load requirement to p95 under 300 ms on 4G." > "Deprecate SCL-FR-3; the client dropped cash on delivery." ## Related - [get_requirement](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-requirement) — the requirement as it stands. - [create_requirement_draft](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/create-requirement-draft) — a new one. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — the road into review and approved. - [Requirements API](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements) — the same doors over REST. --- # update_use_case Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/update-use-case Changes a use case in part, replaces its steps when a list is sent and moves it where the lifecycle allows by hand. Change a use case by `use_case_id` with any of `title`, `actor_persona_id`, `secondary_actors` (replaces the list), `goal`, `trigger`, `preconditions`, `postconditions` (HTML), `frequency`, `priority`, `steps` and `status`. A field left out keeps its value; a field sent as null is cleared. `steps` replaces the list in order: a row with an `id` keeps and changes that step, a row without one is new, and a step left out goes. The change goes through the same action the Use Cases page runs and writes a version; `use_case.updated` goes out. A `status` goes through its own move: `deprecated` or back to `draft` are moves a person may make by hand, and `use_case.status_changed` goes out. `in_review` and `approved` are reached through `submit_for_review` and the approvers, and any move is refused while an approval request is open. A staff token with `use-case:update` on the project is needed. - Requires ability: `use-case:update` - Runs the same action as [`PATCH /v1/use-cases/{useCase}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/update-a-use-case) - Runs the same action as [`POST /v1/use-cases/{useCase}/status`](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases/move-a-use-case) - Fires events: [`use_case.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-updated), [`use_case.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-status-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `use_case_id` | string | yes | UUID of the use case. | | `title` | string | no | What the use case is called, 3 to 200 characters. | | `actor_persona_id` | string | no | UUID of the persona who acts. | | `secondary_actors` | array of string | no | The other actors in the room, in words; the list replaces the current one. | | `goal` | string | no | What the actor is trying to achieve. | | `trigger` | string | no | What starts it. | | `preconditions` | string | no | What must be true before, as HTML. | | `postconditions` | string | no | What is true after, as HTML. | | `frequency` | string | no | How often it happens. | | `priority` | `must`, `should`, `could`, `wont` | no | Its MoSCoW priority; should by default. | | `steps` | array of any | no | The steps replacing the list, in order: objects with actor_action (required), system_response, flow (main, alternate, exception), flow_label and branches_from; a row with an id keeps that step. | | `status` | `draft`, `in_review`, `approved`, `deprecated` | no | A move the lifecycle allows by hand; the review states go through submit_for_review. | ## Example call ```json { "name": "update_use_case", "arguments": { "use_case_id": "ca87f783-5324-4c00-a4b0-8e3a6df90725" } } ``` ## What it returns ```json { "data": { "id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "number": 2, "reference": "SCL-UC-2", "title": "Book a pickup", "actor_persona_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c", "actor_persona_name": "Busy parent", "secondary_actors": ["Driver"], "goal": "Have the laundry collected from home", "trigger": "The customer has an order ready to hand over", "preconditions": "<p>The order is paid.</p>", "postconditions": "<p>A pickup slot is held and the driver's route includes the address.</p>", "frequency": "Daily", "priority": "must", "status": "draft", "steps": [ { "id": "f6a7b8c9-d0e1-4f2a-8b3c-4d5e6f7a8b9c", "use_case_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "position": 1, "flow": "main", "flow_label": null, "actor_action": "Chooses a pickup slot and confirms", "system_response": "Holds the slot and sends the confirmation", "branches_from": null } ], "position": 2, "version": 2, "approved_at": null, "approved_revision_id": null, "created_at": "2026-10-28T10:50:00+00:00", "updated_at": "2026-10-28T13:15:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a priority outside MoSCoW, a persona that is not the project's, a step without an actor action, a flow outside the three, a branch from a step that does not exist, a status of in_review or approved (the message says to submit the use case for review), a move the lifecycle does not allow, or a move while a request is open. - `RESOURCE_NOT_FOUND` — unknown use_case_id, or a use case outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry use-case:update. - `TOKEN_MISSING_ABILITY` — token lacks use-case:update. ## Example prompts > "Merge the first two steps of the pickup use case into one." > "Add an exception flow to SCL-UC-2 for a driver who cannot find the address." > "Deprecate the drop-off use case; the shop stopped taking walk-ins." ## Related - [get_use_case](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/get-use-case) — the use case with its steps as they stand. - [delete_use_case](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/delete-use-case) — take it away. - [submit_for_review](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/submit-for-review) — the road into review and approved. - [Use cases API](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases) — the same doors over REST. --- # create_client Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-client Adds a client to the team's registry, the first record Heimdall writes when a person describes a new client's project in conversation. Create a client on the team's registry: `name` is required and unique within the team; `legal_name`, `website`, `industry`, `country` (a two-letter upper-case code such as `LK`), `timezone`, `primary_contact_name`, `primary_contact_email`, `notes` (HTML) and `status` (`prospect`, `active`, `dormant` or `churned`; `active` by default) fill in what the person said. The client is written through the same action the Clients page runs and `client.created` goes out. Look the client up with `list_clients` first, so a client already on the registry is reused rather than created twice; create the project under it with `create_project` once it exists. A staff token with `client:create` is needed; a client guest's token is refused. - Requires ability: `client:create` - Runs the same action as [`POST /v1/clients`](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/create-a-client) - Fires events: [`client.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The client's name, 2 to 100 characters, unique within the team. | | `legal_name` | string | no | The registered name, up to 160 characters. | | `website` | string | no | The client's site as a full URL. | | `industry` | string | no | The line of business, up to 80 characters. | | `country` | string | no | A two-letter upper-case country code, such as LK. | | `timezone` | string | no | A timezone identifier, such as Asia/Colombo. | | `primary_contact_name` | string | no | Who speaks for the client. | | `primary_contact_email` | string | no | Their email address. | | `notes` | string | no | What the team should know, as HTML. | | `status` | `prospect`, `active`, `dormant`, `churned` | no | Where the relationship stands; active by default. | ## Example call ```json { "name": "create_client", "arguments": { "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "9b1f2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d", "team_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Super Clean Laundry", "legal_name": null, "website": null, "industry": "Laundry services", "country": "LK", "timezone": "Asia/Colombo", "primary_contact_name": "Nadeesha Perera", "primary_contact_email": "nadeesha@example.test", "notes": null, "status": "active", "projects_count": 0, "archived_at": null, "created_at": "2026-10-28T09:12:00+00:00", "updated_at": "2026-10-28T09:12:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — name missing or shorter than 2 characters, a country that is not a two-letter upper-case code, a status outside the four, or a name the team already uses. - `FORBIDDEN` — the token is a client guest's, or the holder's team role does not carry client:create. - `TOKEN_MISSING_ABILITY` — token lacks client:create. ## Example prompts > "We have a new client, Super Clean Laundry in Colombo; Nadeesha Perera is the contact." > "Add Beta Bank as a prospect; they are in retail banking." > "Register the laundry shop as a client and then create its project." ## Related - [list_clients](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-clients) — find the client before creating it. - [update_client](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-client) — fill in what the person says later. - [create_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-project) — the project under the client. - [Clients API](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients) — the same door over REST. --- # create_component Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-component Adds one of the applications a project is made of, so Heimdall can lay down the web app and the API of a new project on the house stack. Add a component to a project: `project_id` and `name` are required; `slug` is derived from the name when left out and names the component in versions such as `web@1.4.0`; `kind` is `web_app` by default or one of `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure`, `other`; `description` (HTML), `tech_stack` (short tags) and `owner_id` fill in the rest. The component is written through the same action the Components page and the intake wizard run, and `component.created` goes out. Assume the house stack unless the brief rules it out: a Laravel 13 web app with Livewire 4 and Flux, a Laravel API where one is needed, PostgreSQL and Redis behind them. A staff token with `component:create` on the project is needed. - Requires ability: `component:create` - Runs the same action as [`POST /v1/projects/{project}/components`](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/create-a-component) - Fires events: [`component.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `name` | string | yes | What the component is called within the project, 2 to 80 characters. | | `slug` | string | no | The slug shown in versions such as web@1.4.0; derived from the name when left out. | | `kind` | `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure`, `other` | no | What the component is; web_app by default. | | `description` | string | no | What the component does, as HTML. | | `tech_stack` | array of string | no | Short technology tags, such as Laravel 13 and PostgreSQL. | | `owner_id` | string | no | UUID of the team member who answers for it. | ## Example call ```json { "name": "create_component", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Web app", "slug": "web-app", "kind": "web_app", "description": "<p>The customer-facing site: services, ordering, tracking and bookings.</p>", "tech_stack": ["Laravel 13", "Livewire 4", "Flux UI", "PostgreSQL"], "owner_id": null, "position": 1, "environments_count": 0, "archived_at": null, "created_at": "2026-10-28T09:18:00+00:00", "updated_at": "2026-10-28T09:18:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — name missing or outside 2 to 80 characters, a kind outside the list, a slug or name another component of the project already uses, more tags than allowed, or an owner_id who is not on the team. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry component:create. - `TOKEN_MISSING_ABILITY` — token lacks component:create. ## Example prompts > "Add the web app and the API to the laundry project on our usual stack." > "The client also wants a driver app for Android; add it as a component." > "Create an integration component for the payment gateway." ## Related - [list_components](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) — what the project is made of today. - [create_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-environment) — where the component runs. - [update_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-component) — change it later. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components) — the same door over REST. --- # create_design_reference Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-design-reference Adds a link to a design, so the Figma file or the API contract a person pastes into the conversation lands on the record. Add a design reference to a project: `project_id`, `title` and `url` are required; `kind` is `other` by default or one of `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document`; `referenceable_type` is `project` by default or `component` with the component's id in `referenceable_id`; `version` and `notes` (HTML) fill in the rest. The reference is written through the same action the Designs page and the intake wizard run, and `design_reference.created` goes out. A staff token with `design-reference:create` on the project is needed. - Requires ability: `design-reference:create` - Runs the same action as [`POST /v1/projects/{project}/design-references`](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/add-a-design-reference) - Fires events: [`design_reference.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `title` | string | yes | What the reference is called, 2 to 160 characters. | | `url` | string | yes | Where the design lives, as a full URL. | | `kind` | `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document`, `other` | no | What the link points at; other by default. | | `referenceable_type` | `project`, `component` | no | What the reference hangs off: project by default, or component. | | `referenceable_id` | string | no | UUID of the component when the target is a component. | | `version` | string | no | The version or revision of the design, as the designer labels it. | | `notes` | string | no | What to look at and why, as HTML. | ## Example call ```json { "name": "create_design_reference", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "title": "<title>", "url": "https://example.com" } } ``` ## What it returns ```json { "data": { "id": "3b4c5d6e-7f8a-4b9c-8d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "referenceable_type": "component", "referenceable_id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "kind": "figma", "title": "Order flow", "url": "https://www.figma.com/file/abc123/order-flow", "version": "v1", "notes": "<p>The three screens of placing an order; the payment step is a placeholder.</p>", "added_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "position": 1, "created_at": "2026-10-28T09:40:00+00:00", "updated_at": "2026-10-28T09:40:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — title or url missing, a url that is not an address, a kind or target outside its set, or a component target without a component of the project. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry design-reference:create. - `TOKEN_MISSING_ABILITY` — token lacks design-reference:create. ## Example prompts > "Here is the Figma for the order flow: https://www.figma.com/file/abc123. Attach it to the web app." > "Add the payment gateway's API contract as a design reference." > "Save this ERD link on the project." ## Related - [list_design_references](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-design-references) — the designs linked today. - [update_design_reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-design-reference) — change a link later. - [Design references API](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references) — the same door over REST. --- # create_environment Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-environment Adds one place a project runs, so Heimdall can lay down the development, staging, UAT and production rows of a new project. Add an environment to a project: `project_id` and `name` are required; `kind` is `development` by default or one of `local`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom`; `component_id` names the component it runs (omit it when it serves the whole project); `url`, `api_url`, `admin_url`, `branch`, `hosting`, `region`, `auto_deploy`, `protected` (a deployment then needs a promotion approval), `health_check_url`, `current_version` and `notes` (HTML) fill in the rest. The environment is written through the same action the Environments page and the intake wizard run, and `environment.created` goes out. The health is recorded by monitoring and a freeze is a release manager's call in the application; neither is written here. A staff token with `environment:create` on the project is needed. - Requires ability: `environment:create` - Runs the same action as [`POST /v1/projects/{project}/environments`](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/create-an-environment) - Fires events: [`environment.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `name` | string | yes | What the environment is called, such as Staging, 2 to 60 characters. | | `kind` | `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom` | no | What the environment is for; development by default. | | `component_id` | string | no | UUID of the component it runs; omit when it serves the whole project. | | `url` | string | no | The address people open. | | `api_url` | string | no | The address the API answers on. | | `admin_url` | string | no | The address of the back office. | | `branch` | string | no | The branch deployed there. | | `hosting` | string | no | Where it runs, in words. | | `region` | string | no | The region it runs in. | | `auto_deploy` | boolean | no | Whether the pipeline deploys to it without a hand. | | `protected` | boolean | no | Whether a deployment needs a promotion approval. | | `health_check_url` | string | no | The address the scheduled health check calls. | | `current_version` | string | no | The version running there, as last recorded. | | `notes` | string | no | Anything the team should know, as HTML. | ## Example call ```json { "name": "create_environment", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9", "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "name": "Staging", "kind": "staging", "url": "https://staging.example.test", "api_url": null, "admin_url": null, "branch": "develop", "hosting": null, "region": null, "auto_deploy": true, "protected": false, "health": "unknown", "health_check_url": null, "last_checked_at": null, "consecutive_failures": 0, "current_version": null, "frozen_until": null, "frozen": false, "notes": null, "position": 2, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-10-28T09:20:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — name missing or outside 2 to 60 characters, a kind outside the list, an address that is not a URL, a component_id that is not the project's, or a name already used for the same component. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry environment:create. - `TOKEN_MISSING_ABILITY` — token lacks environment:create. ## Example prompts > "Set up development, staging, UAT and production environments for the laundry web app." > "Add a production environment at https://app.example.test and protect it." > "Register the staging API environment on the develop branch." ## Related - [list_environments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) — where the project runs today. - [update_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-environment) — change it later. - [create_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-component) — the component an environment runs. - [Environments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments) — the same door over REST. --- # create_project Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-project Creates a project on the team's registry from a name and a paragraph, with the key suggested when none is given, so Heimdall can open a project in conversation. Create a project: `name` is required and unique within the team; `key`, two to six upper-case letters and digits starting with a letter, is the prefix of every reference inside the project (`SCL-12`) and is suggested from the name when left out; `client_id` names one of the team's clients (omit it for an internal project); `description` holds the brief as HTML; `tagline`, `status` (`discovery` by default), `methodology` (`scrum` by default), `visibility` (`team` by default), the four lead ids, `starts_on`, `target_launch_on`, `launched_on` and `budget_hours` fill in the rest. The project is written through the same action the registry page and the intake wizard run, and `project.created` goes out. This is the door the conversational intake opens: find or create the client, create the project with the brief as its description, then write the record under it with the component, environment, stakeholder, persona, epic, story, requirement, rule and glossary tools, and convene the council on the project when the scaffold is in. The answer carries the key chosen, which every later reference uses. A staff token with `project:create` is needed. - Requires ability: `project:create` - Runs the same action as [`POST /v1/projects`](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/create-a-project) - Fires events: [`project.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-created) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The project's name, 3 to 100 characters, unique within the team. | | `key` | string | no | Two to six upper-case letters and digits starting with a letter, such as SCL; suggested from the name when left out. | | `client_id` | string | no | UUID of the client the project is for; omit for an internal project. | | `tagline` | string | no | One line about the project, up to 160 characters. | | `description` | string | no | What the project is, as HTML; the brief as the person gave it fits here. | | `status` | `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` | no | The lifecycle status; discovery by default. | | `methodology` | `scrum`, `kanban`, `hybrid`, `waterfall` | no | How the project is run; scrum by default. | | `visibility` | `team`, `members_only` | no | team when every member sees it, members_only when only its project members do. | | `lead_pm_id` | string | no | UUID of the project manager, a member of the team. | | `lead_ba_id` | string | no | UUID of the lead business analyst. | | `lead_dev_id` | string | no | UUID of the lead developer. | | `lead_qa_id` | string | no | UUID of the QA lead. | | `starts_on` | string | no | The planned start, YYYY-MM-DD. | | `target_launch_on` | string | no | The target launch, YYYY-MM-DD, on or after the start. | | `launched_on` | string | no | The day the project went live, YYYY-MM-DD. | | `budget_hours` | integer | no | The budgeted effort in whole hours. | ## Example call ```json { "name": "create_project", "arguments": { "name": "<name>" } } ``` ## What it returns ```json { "data": { "id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "team_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "client_id": "9b1f2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d", "key": "SCL", "name": "Super Clean Laundry", "tagline": "Laundry orders, tracking, payment and pickups online", "description": "<p>An informational website with laundry order placement, tracking, payment and appointment scheduling.</p>", "status": "discovery", "methodology": "scrum", "visibility": "team", "health": "unknown", "lead_pm_id": null, "lead_ba_id": null, "lead_dev_id": null, "lead_qa_id": null, "starts_on": null, "target_launch_on": null, "launched_on": null, "budget_hours": null, "archived_at": null, "created_at": "2026-10-28T09:15:00+00:00", "updated_at": "2026-10-28T09:15:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — name missing or outside 3 to 100 characters, a key that is not two to six upper-case letters and digits starting with a letter, a key or a name the team already had, a client_id that is not the team's, a lead who is not on the team, a launch before the start, or status set to archived. - `FORBIDDEN` — the token is a client guest's, or the holder's team role does not carry project:create. - `TOKEN_MISSING_ABILITY` — token lacks project:create. ## Example prompts > "Create a project for Super Clean Laundry: an informational website with online ordering, tracking, payment and appointment booking." > "Open a Kanban project called Field Service Portal for Acme Retail, key FSP." > "Start an internal project for our own website refresh." ## Related - [create_client](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-client) — the client first, when it is new. - [update_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project) — the dates, the leads and the client later. - [create_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-component) — the first thing under a new project. - [convene_council](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/planning/convene-council) — the council on the scaffold once it is in. - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects) — the same door over REST. --- # delete_component Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-component Deletes a component of a project after the person confirms; the environments and repositories under it lose their component. Delete a component by `component_id`. The environments and repositories that hang off it stay and lose their component; work items that touched it lose the tag. The removal goes through the same action the Components page runs and `component.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. Prefer changing a component over deleting one that work items already touch. A staff token with `component:delete` on the project is needed. - Requires ability: `component:delete` - Runs the same action as [`DELETE /v1/components/{component}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/delete-a-component) - Fires events: [`component.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `component_id` | string | yes | UUID of the component to delete. | ## Example call ```json { "name": "delete_component", "arguments": { "component_id": "90828f38-f4fe-4c00-aeb5-f681a0715508" } } ``` ## What it returns ```json { "data": { "component_id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown component_id, or a component outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry component:delete. - `TOKEN_MISSING_ABILITY` — token lacks component:delete. ## Example prompts > "Remove the Android app component; they are not doing a native app after all." > "Delete the duplicate API component." ## Related - [update_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-component) — change it instead. - [list_components](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) — what is left. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components) — the same door over REST. --- # delete_design_reference Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-design-reference Deletes a design reference of a project after the person confirms; the design itself is untouched. Delete a design reference by `design_reference_id`. Only the link goes; the design lives where it lived. The removal goes through the same action the Designs page runs and `design_reference.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. A staff token with `design-reference:delete` on the project is needed. - Requires ability: `design-reference:delete` - Runs the same action as [`DELETE /v1/design-references/{reference}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/delete-a-design-reference) - Fires events: [`design_reference.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `design_reference_id` | string | yes | UUID of the design reference to delete. | ## Example call ```json { "name": "delete_design_reference", "arguments": { "design_reference_id": "6ee4976b-499b-4400-af2c-112ee89091fd" } } ``` ## What it returns ```json { "data": { "design_reference_id": "3b4c5d6e-7f8a-4b9c-8d0e-1f2a3b4c5d6e", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown design_reference_id, or a reference outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry design-reference:delete. - `TOKEN_MISSING_ABILITY` — token lacks design-reference:delete. ## Example prompts > "Remove the old wireframe link; the Figma replaces it." > "Delete the broken prototype reference." ## Related - [update_design_reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-design-reference) — change it instead. - [list_design_references](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-design-references) — what is left. - [Design references API](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references) — the same door over REST. --- # delete_environment Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-environment Deletes an environment of a project after the person confirms. Delete an environment by `environment_id`. The removal goes through the same action the Environments page runs and `environment.deleted` goes out. The tool is destructive: a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. A staff token with `environment:delete` on the project is needed. - Requires ability: `environment:delete` - Runs the same action as [`DELETE /v1/environments/{environment}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/delete-an-environment) - Fires events: [`environment.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-deleted) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `environment_id` | string | yes | UUID of the environment to delete. | ## Example call ```json { "name": "delete_environment", "arguments": { "environment_id": "7dc196f2-3d2f-4000-a1c0-910b57fc0c9f" } } ``` ## What it returns ```json { "data": { "environment_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", "deleted": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown environment_id, or an environment outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry environment:delete. - `TOKEN_MISSING_ABILITY` — token lacks environment:delete. ## Example prompts > "Remove the preproduction environment; we are not keeping one." > "Delete the old QA environment of the API." ## Related - [update_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-environment) — change it instead. - [list_environments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) — what is left. - [Environments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments) — the same door over REST. --- # get_client Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-client Fetch a single client by UUID — its name, contacts, status and notes. Fetch one client in full once [`list_clients`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-clients) has named it or a project's `client_id` has pointed at it: the registered name, the website, the country and timezone, the primary contact, the notes the team keeps and where the relationship stands. A client outside the token's team, or one a guest's projects do not name, answers `RESOURCE_NOT_FOUND`, never a permission error, so existence does not leak. - Requires ability: `client:view` - Runs the same action as [`GET /v1/clients/{client}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/get-a-client) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | yes | UUID of the client to fetch. | ## Example call ```json { "name": "get_client", "arguments": { "client_id": "d95d0d1e-99fb-4000-a76a-74a721165572" } } ``` ## What it returns ```json { "data": { "id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "name": "Acme Retail", "legal_name": "Acme Retail (Pvt) Ltd", "website": "https://acme.example", "industry": "Retail", "country": "LK", "timezone": "Asia/Colombo", "primary_contact_name": "Jane Perera", "primary_contact_email": "jane@acme.example", "notes": "<p>Prefers a weekly status call on Tuesdays.</p>", "status": "active", "archived_at": null, "created_at": "2026-08-20T10:05:00+00:00", "updated_at": "2026-10-24T16:40:00+00:00" }, "meta": {} } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown client_id, or a client outside the token's team or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks client:view. ## Example prompts > "Who is the primary contact at Acme Retail?" > "Get the client `c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d` and tell me which timezone they work in." ## Related - [list_clients](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-clients) - [get_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project) — a project of the client. - [Clients API](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients) --- # get_component Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-component Fetch a single component by UUID — its name, slug, kind, description, tech stack, owner and archive stamp. Fetch one component in full once [`list_components`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) has named it, or once an environment's `component_id` has: what the application is, the slug its versions carry, the technologies it is built with and who answers for it. A component outside the token's team or project scope, or one the person cannot see, answers `RESOURCE_NOT_FOUND`, never a permission error, so existence does not leak. - Requires ability: `component:view` - Runs the same action as [`GET /v1/components/{component}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/get-a-component) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `component_id` | string | yes | UUID of the component to fetch. | ## Example call ```json { "name": "get_component", "arguments": { "component_id": "90828f38-f4fe-4c00-aeb5-f681a0715508" } } ``` ## What it returns ```json { "data": { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Customer Web App", "slug": "web", "kind": "web_app", "description": "<p>The portal account holders sign in to.</p>", "tech_stack": ["Laravel 13", "Livewire 4", "PostgreSQL 18"], "owner_id": "5e0c7a2d-9b1f-4c3e-8d6a-2f4b7c9e1a30", "position": 0, "archived_at": null, "created_at": "2026-10-20T09:00:00+00:00", "updated_at": "2026-10-27T11:25:00+00:00" }, "meta": {} } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown component_id, or a component outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks component:view. ## Example prompts > "Get component `d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70`." > "What is the web component of this project built with, and who owns it?" ## Related - [list_components](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) - [list_environments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) — the environments that run the component. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components) --- # get_design_reference Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-design-reference One design reference by id, with its address, kind, what it is attached to, version, notes and who added it. Return one design reference of a project: the title, the address to open, the kind, whether it hangs off the whole project or one component and which, the version the designer labels it with, the notes as HTML and the user who added it. Read it when a story, a requirement or a brief names a design you need to open, or to follow a reference id from a listing. A reference of a project the token may not see is unknown rather than forbidden. - Requires ability: `design-reference:view` - Runs the same action as [`GET /v1/design-references/{reference}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/get-a-design-reference) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `design_reference_id` | string | yes | UUID of the design reference to fetch. | ## Example call ```json { "name": "get_design_reference", "arguments": { "design_reference_id": "6ee4976b-499b-4400-af2c-112ee89091fd" } } ``` ## What it returns ```json { "data": { "id": "e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "referenceable_type": "component", "referenceable_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "kind": "figma", "title": "Checkout flow", "url": "https://www.figma.com/file/aB3dE5fG7hJ9kL1mN3pQ5r/Checkout", "version": "v3", "notes": "<p>Frames 12 to 18 are the signed-off flow; the rest are explorations.</p>", "added_by": "9b2e4f1a-3c5d-4e6f-8a7b-1c2d3e4f5a6b", "position": 0, "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown design_reference_id, or a reference of a project outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks design-reference:view. ## Example prompts > "Open the design reference `e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a01` and tell me which frames are signed off." > "What version is the checkout flow design at?" > "Who added the payments API contract?" ## Related - [list_design_references](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-design-references) - [get_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-component) — the component a reference is attached to. - [Design References API](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references) --- # get_environment Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-environment Fetch a single environment by UUID — its kind, component, addresses, branch, flags, health, version and freeze, never a secret. Fetch one environment in full once [`list_environments`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) has named it: the addresses a browser or a pipeline opens, the branch and version deployed there, whether deployments need a promotion approval, what the last health check found and when, and whether a change freeze is in force. The record never carries a secret. An environment outside the token's team or project scope, one the person cannot see, or one a client guest's token may not read (anything but UAT and production) answers `RESOURCE_NOT_FOUND`, never a permission error, so existence does not leak. - Requires ability: `environment:view` - Runs the same action as [`GET /v1/environments/{environment}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/get-an-environment) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `environment_id` | string | yes | UUID of the environment to fetch. | ## Example call ```json { "name": "get_environment", "arguments": { "environment_id": "7dc196f2-3d2f-4000-a1c0-910b57fc0c9f" } } ``` ## What it returns ```json { "data": { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": null, "name": "Production", "kind": "production", "url": "https://app.example.test", "api_url": "https://api.example.test", "admin_url": "https://app.example.test/admin", "branch": "main", "hosting": "Managed containers", "region": "Singapore", "auto_deploy": false, "protected": true, "health": "healthy", "health_check_url": "https://app.example.test/up", "last_checked_at": "2026-10-27T11:25:00+00:00", "consecutive_failures": 0, "current_version": "1.3.2", "frozen_until": "2026-11-01T00:00:00+00:00", "frozen": true, "notes": null, "position": 6, "created_at": "2026-10-20T09:05:00+00:00", "updated_at": "2026-10-27T11:25:00+00:00" }, "meta": {} } ``` A guest's token receives the same record without `admin_url`, `branch`, `hosting`, `region`, `health_check_url`, `consecutive_failures` and `notes`. ## How it fails - `RESOURCE_NOT_FOUND` — unknown environment_id, an environment outside the token's scope or the person's visibility, or one a guest may not read. - `TOKEN_MISSING_ABILITY` — token lacks environment:view. ## Example prompts > "Get environment `a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d`." > "What version is on production, is it healthy, and is it frozen?" ## Related - [list_environments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) - [get_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-component) — the application the environment runs. - [Environments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments) --- # get_project_settings Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project-settings A project's settings by project id, every key present with the workspace default filled in, from sprint length and working days to estimation, workflow flags, business hours and alert routing. Return a project's settings with every key present: how long a sprint runs and on which days, the timezone deadlines are read in, what work is sized in and on which scale, who a new item goes to, the workflow flags, whether guests may comment and Heimdall assists, the Telegram group, the board's limits and hidden columns, the business hours and whether a production outage opens an incident. Read it before planning a sprint, sizing a story or reading a deadline, so you count the days, the hours and the points the way the project does. A guest token is refused: the settings are the team's, not the client's. - Requires ability: `project:view` - Runs the same action as [`GET /v1/projects/{project}/settings`](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/get-a-projects-settings) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose settings to fetch. | ## Example call ```json { "name": "get_project_settings", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "sprint_length_days": 14, "working_days": [1, 2, 3, 4, 5], "timezone": "Asia/Colombo", "estimation_unit": "points", "story_point_scale": "fibonacci", "default_assignee_rule": "reporter", "auto_transition_on_pr": true, "auto_transition_on_merge": true, "require_dor_for_sprint": false, "require_dod_for_done": true, "guest_can_comment": true, "ai_assist_enabled": true, "telegram_group_chat_id": "-1001234567890", "wip_limits": {}, "hidden_columns": [], "business_hours": { "start": "09:00", "end": "18:00" }, "auto_incident_on_down": true } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token belongs to a client guest, who may read the project but not its settings. - `TOKEN_MISSING_ABILITY` — token lacks project:view. ## Example prompts > "How long are the Customer Portal's sprints, and which days count?" > "Read the settings of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` and tell me whether stories are sized in points." > "Does a merged pull request move the story on in this project?" ## Related - [get_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project) — the project's own record. - [list_environments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) — the environments the incident switch watches. - [Project Settings API](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings) --- # get_project Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project Fetch a single project by UUID — its key, status, methodology, visibility, health, leads and dates. Fetch one project in full once [`list_projects`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-projects) has named it: the key every reference inside it carries, where it is in its lifecycle, how the team works it, who leads each discipline and the dates it runs between. A project outside the token's team or project scope, or one the person cannot see, answers `RESOURCE_NOT_FOUND`, never a permission error, so existence does not leak. - Requires ability: `project:view` - Runs the same action as [`GET /v1/projects/{project}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/get-a-project) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project to fetch. | ## Example call ```json { "name": "get_project", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": { "id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "client_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "key": "CRM", "name": "Customer Portal", "tagline": "Self-service portal for the client's account holders", "description": "<p>Account holders manage invoices, tickets and contacts without calling support.</p>", "status": "in_development", "methodology": "scrum", "visibility": "team", "health": "on_track", "lead_pm_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "lead_ba_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "lead_dev_id": "5e0c7a2d-9b1f-4c3e-8d6a-2f4b7c9e1a30", "lead_qa_id": null, "starts_on": "2026-09-01", "target_launch_on": "2027-01-15", "launched_on": null, "budget_hours": 1200, "archived_at": null, "created_at": "2026-08-20T10:05:00+00:00", "updated_at": "2026-10-24T16:40:00+00:00" }, "meta": {} } ``` ## How it fails - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks project:view. ## Example prompts > "Get details for project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`." > "What is the status and health of the Customer Portal project, and who leads development?" ## Related - [list_projects](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-projects) - [get_activity_log](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-activity-log) — the project's history. - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects) --- # Project Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project A project is one client engagement's record: its key, which prefixes every reference inside it, its status from discovery to maintenance, its methodology, visibility, health and leads. These tools read the registry as the token sees it, the clients the projects are done for, the components each project is made of (its web app, API, mobile apps and services, with the slug their versions carry and the stack they are built with) and the environments each runs in (their addresses, the branch deployed there, the flags, the health the last check found and any change freeze) and the design references each keeps (links to Figma files, wireframes, prototypes, screenshots, API contracts and diagrams, attached to the whole project or to one component, with the version that is current); and, as far as the token's abilities allow, create and change them: a client (`create_client`, `update_client`), a project with a key suggested from its name (`create_project`, `update_project`) and its settings (`update_project_settings`), and the components, environments and design references under it, each with a delete that pauses for confirmation. Archiving and deleting a project, the health a monitor records and a change freeze stay in the application and the REST API, and the build, test, release and maintenance tools join as each module lands. ## Tools - [`create_client`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-client) — Create Client (destructive) - [`create_component`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-component) — Create Component (destructive) - [`create_design_reference`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-design-reference) — Create Design Reference (destructive) - [`create_environment`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-environment) — Create Environment (destructive) - [`create_project`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-project) — Create Project (destructive) - [`delete_component`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-component) — Delete Component (destructive) - [`delete_design_reference`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-design-reference) — Delete Design Reference (destructive) - [`delete_environment`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-environment) — Delete Environment (destructive) - [`get_client`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-client) — Get Client (read) - [`get_component`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-component) — Get Component (read) - [`get_design_reference`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-design-reference) — Get Design Reference (read) - [`get_environment`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-environment) — Get Environment (read) - [`get_project`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project) — Get Project (read) - [`get_project_settings`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project-settings) — Get Project Settings (read) - [`list_clients`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-clients) — List Clients (read) - [`list_components`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) — List Components (read) - [`list_design_references`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-design-references) — List Design References (read) - [`list_environments`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) — List Environments (read) - [`list_projects`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-projects) — List Projects (read) - [`update_client`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-client) — Update Client (destructive) - [`update_component`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-component) — Update Component (destructive) - [`update_design_reference`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-design-reference) — Update Design Reference (destructive) - [`update_environment`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-environment) — Update Environment (destructive) - [`update_project`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project) — Update Project (destructive) - [`update_project_settings`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project-settings) — Update Project Settings (destructive) --- # list_clients Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-clients Paginated list of the clients the token's team does projects for, narrowed by status, search words or archived state. Return the clients of the token's team, alphabetically, with how many projects each has; a guest sees only the clients of the projects they were invited into. Use it to find the `client_id` a project names, to pick a client before describing a new project, or to answer who the team works for and where each relationship stands. Narrow with `status` (`prospect`, `active`, `dormant`, `churned`), `search` (words in the name, the legal name or the primary contact email) and `archived` (`true` for archived clients only, `false` for live ones; omit for both). - Requires ability: `client:view-any` - Runs the same action as [`GET /v1/clients`](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/list-clients) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `prospect`, `active`, `dormant`, `churned` | no | Narrow to one status. | | `search` | string | no | Words the name, legal name or primary contact email must contain. | | `archived` | boolean | no | `true` lists archived clients only, `false` live ones only; omit for both. | | `limit` | integer | no | Clients per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_clients", "arguments": { "status": "prospect", "search": "<search>" } } ``` ## What it returns ```json { "data": [ { "id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "name": "Acme Retail", "legal_name": "Acme Retail (Pvt) Ltd", "website": "https://acme.example", "industry": "Retail", "country": "LK", "timezone": "Asia/Colombo", "primary_contact_name": "Jane Perera", "primary_contact_email": "jane@acme.example", "notes": null, "status": "active", "projects_count": 2, "archived_at": null, "created_at": "2026-08-20T10:05:00+00:00", "updated_at": "2026-10-24T16:40:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — status is not one of the four statuses. - `TOKEN_MISSING_ABILITY` — token lacks client:view-any. ## Example prompts > "Which clients do we work for?" > "List the dormant clients so I can see who to follow up with." > "Find the client id for Acme Retail." ## Related - [get_client](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-client) - [list_projects](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-projects) — the projects done for a client, filtered by client_id. - [Clients API](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients) --- # list_components Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components Paginated list of the components a project is made of, with how many environments run each, narrowed by kind, search words or archived state. Return the components of one project in the project's order: the web app, the API, the mobile apps, the services and the platform, each with its `slug` (what a version string names it by, as in `web@1.4.0`), its `kind`, its `tech_stack` tags, its `owner_id` and `environments_count`. Read it before writing code for a project, so you know which application a story touches and what it is built with, and to find the `component_id` an environment or a story names. Narrow with `kind` (`web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure`, `other`), `search` (words in the name or the slug) and `archived` (`true` for archived components only, `false` for live ones; omit for both). - Requires ability: `component:view-any` - Runs the same action as [`GET /v1/projects/{project}/components`](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/list-components) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose components to list. | | `kind` | `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure`, `other` | no | Narrow to one kind. | | `search` | string | no | Words the name or the slug must contain. | | `archived` | boolean | no | `true` lists archived components only, `false` live ones only; omit for both. | | `limit` | integer | no | Components per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_components", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Customer Web App", "slug": "web", "kind": "web_app", "description": "<p>The portal account holders sign in to.</p>", "tech_stack": ["Laravel 13", "Livewire 4", "PostgreSQL 18"], "owner_id": "5e0c7a2d-9b1f-4c3e-8d6a-2f4b7c9e1a30", "position": 0, "environments_count": 3, "archived_at": null, "created_at": "2026-10-20T09:00:00+00:00", "updated_at": "2026-10-27T11:25:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — kind is not one of the eleven kinds. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks component:view-any. ## Example prompts > "What is the Customer Portal project made of?" > "List the API components of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` and what each is built with." > "Which components of this project have no environment yet?" ## Related - [get_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-component) - [list_environments](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments) — where a component runs, filtered by component_id. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components) --- # list_design_references Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-design-references Paginated list of the links a project keeps to its designs, each with its kind, what it is attached to, version and notes, narrowed by kind, component, whole-project scope or search words. Return the design references of one project in the project's order: the links the analyst and the designer pinned to Figma files, wireframes, prototypes, screenshots, API contracts, diagrams and documents, each with its title, address, kind, what it is attached to (the whole project or one component), version, notes as HTML and who added it. Read it before implementing a screen or a contract, so you build against the design the team linked rather than guessing it, and to find the current version of a design. Narrow with `kind` (`figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document` or `other`), `component_id` (the references attached to one component), `project_wide` (`true` for the references attached to the whole project only) and `search` (words in the title, the address or the version). A guest token reads every reference of its project. - Requires ability: `design-reference:view-any` - Runs the same action as [`GET /v1/projects/{project}/design-references`](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/list-design-references) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose design references to list. | | `kind` | string | no | One kind: figma, wireframe, prototype, screenshot, api_contract, sequence_diagram, erd, document or other. | | `component_id` | string | no | UUID of a component; lists the references attached to it only. | | `project_wide` | boolean | no | `true` lists the references attached to the whole project only. | | `search` | string | no | Words the title, the address or the version must contain. | | `limit` | integer | no | References per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_design_references", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "referenceable_type": "component", "referenceable_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "kind": "figma", "title": "Checkout flow", "url": "https://www.figma.com/file/aB3dE5fG7hJ9kL1mN3pQ5r/Checkout", "version": "v3", "notes": "<p>Frames 12 to 18 are the signed-off flow; the rest are explorations.</p>", "added_by": "9b2e4f1a-3c5d-4e6f-8a7b-1c2d3e4f5a6b", "position": 0, "created_at": "2026-10-20T09:20:00+00:00", "updated_at": "2026-10-27T11:30:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — kind is not one of the nine kinds, or component_id is not a UUID. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks design-reference:view-any. ## Example prompts > "Which designs does the Customer Portal have for the web app?" > "List the API contracts of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13`." > "Which Figma file is current for the checkout flow?" ## Related - [get_design_reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-design-reference) - [list_components](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) — the components a reference may be attached to. - [Design References API](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references) --- # list_environments Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-environments Paginated list of a project's environments in promotion order, with their addresses, branch, flags and health, narrowed by kind, component, health or search words. Return the environments of one project in the project's order, local to production: each with its `kind`, the `component_id` it runs (null for the whole project), its addresses, the `branch` deployed there, the `auto_deploy` and `protected` flags, the `health` the last check found and whether a change freeze is in force. Read it to find where a build lives, which branch is on staging, whether production is frozen, and the `environment_id` a deployment or a test run will name. Narrow with `kind` (`local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom`), `component_id`, `health` (`healthy`, `degraded`, `down`, `unknown`) and `search` (words in the name, an address or the hosting note). A client guest's token lists the UAT and production environments only, without the admin address, the branch, the hosting note, the region, the check's address and streak, and the notes. - Requires ability: `environment:view-any` - Runs the same action as [`GET /v1/projects/{project}/environments`](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/list-environments) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project whose environments to list. | | `kind` | `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom` | no | Narrow to one kind. | | `component_id` | string | no | Narrow to the environments of one component, by UUID. | | `health` | `healthy`, `degraded`, `down`, `unknown` | no | Narrow to one health. | | `search` | string | no | Words the name, the address or the hosting note must contain. | | `limit` | integer | no | Environments per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_environments", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": [ { "id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7081", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "name": "Staging", "kind": "staging", "url": "https://staging.example.test", "api_url": "https://api.staging.example.test", "admin_url": "https://staging.example.test/admin", "branch": "develop", "hosting": "Managed containers", "region": "Singapore", "auto_deploy": true, "protected": false, "health": "healthy", "health_check_url": "https://staging.example.test/up", "last_checked_at": "2026-10-27T11:25:00+00:00", "consecutive_failures": 0, "current_version": "1.4.0", "frozen_until": null, "frozen": false, "notes": "<p>Resets its database every Sunday night.</p>", "position": 2, "created_at": "2026-10-20T09:05:00+00:00", "updated_at": "2026-10-27T11:25:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 1, "has_more": false } } ``` ## How it fails - `VALIDATION_FAILED` — kind or health is not one of its values, or component_id is not a UUID. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `TOKEN_MISSING_ABILITY` — token lacks environment:view-any. ## Example prompts > "Where does the Customer Portal run, and which branch is on each environment?" > "Which environments of project `7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13` are down?" > "Is production frozen right now?" ## Related - [get_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-environment) - [list_components](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-components) — the applications the environments run. - [Environments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments) --- # list_projects Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/list-projects Paginated list of the projects the token can see, narrowed by status, client, search words or archived state. Return every project the token's team can see, paginated: team-visible projects and the members-only projects the person belongs to, with a guest seeing only the projects they were invited into. Use this as a starter tool: most other tools take a `project_id` picked up here, and a project's key (such as `CRM`) prefixes every reference inside it (`CRM-142`). Narrow with `status` (`discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold`, `archived`), `client_id` (a UUID), `search` (words in the key or name) and `archived` (`true` for archived projects only, `false` for live ones; omit for both). - Requires ability: `project:view-any` - Runs the same action as [`GET /v1/projects`](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/list-projects) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold`, `archived` | no | Narrow to one lifecycle status. | | `client_id` | string | no | Narrow to the projects of one client, by UUID. | | `search` | string | no | Words the project key or name must contain. | | `archived` | boolean | no | `true` includes archived projects, which are left out by default. | | `limit` | integer | no | Projects per page (1..100, default 25). | | `page` | integer | no | 1-indexed page number. | ## Example call ```json { "name": "list_projects", "arguments": { "status": "discovery", "client_id": "d95d0d1e-99fb-4000-a76a-74a721165572" } } ``` ## What it returns ```json { "data": [ { "id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "client_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "key": "CRM", "name": "Customer Portal", "tagline": "Self-service portal for the client's account holders", "description": null, "status": "in_development", "methodology": "scrum", "visibility": "team", "health": "on_track", "lead_pm_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "lead_ba_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "lead_dev_id": "5e0c7a2d-9b1f-4c3e-8d6a-2f4b7c9e1a30", "lead_qa_id": null, "starts_on": "2026-09-01", "target_launch_on": "2027-01-15", "launched_on": null, "budget_hours": 1200, "archived_at": null, "created_at": "2026-08-20T10:05:00+00:00", "updated_at": "2026-10-24T16:40:00+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 12, "has_more": false } } ``` `health` is recomputed by the application from overdue items, failing runs, open incidents and stuck approvals; `unknown` means nothing has been measured yet. ## How it fails - `TOKEN_MISSING_ABILITY` — token lacks project:view-any. - `VALIDATION_FAILED` — status is not one of the eight statuses, or client_id is not a UUID. ## Example prompts > "List my Zuse Bifrost projects." > "Which projects are in testing or about to go live?" > "Show me the archived projects for client Acme." ## Related - [get_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project) — one project in full. - [get_activity_log](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/observability/get-activity-log) — what changed on a project and who changed it. - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects) --- # update_client Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-client Changes a client on the team's registry in part, so a contact, a country or a status can be filled in as the conversation reveals it. Change a client by `client_id` with any of `name`, `legal_name`, `website`, `industry`, `country`, `timezone`, `primary_contact_name`, `primary_contact_email`, `notes` (HTML) and `status`. A field left out keeps its value; a field sent as null is cleared. The change goes through the same action the Clients page runs and `client.updated` goes out with what moved. A staff token with `client:update` is needed. An unknown id, or one outside the token's scope, is `RESOURCE_NOT_FOUND`. - Requires ability: `client:update` - Runs the same action as [`PATCH /v1/clients/{client}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients/update-a-client) - Fires events: [`client.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | yes | UUID of the client. | | `name` | string | no | The client's name, 2 to 100 characters, unique within the team. | | `legal_name` | string | no | The registered name, up to 160 characters. | | `website` | string | no | The client's site as a full URL. | | `industry` | string | no | The line of business, up to 80 characters. | | `country` | string | no | A two-letter upper-case country code, such as LK. | | `timezone` | string | no | A timezone identifier, such as Asia/Colombo. | | `primary_contact_name` | string | no | Who speaks for the client. | | `primary_contact_email` | string | no | Their email address. | | `notes` | string | no | What the team should know, as HTML. | | `status` | `prospect`, `active`, `dormant`, `churned` | no | Where the relationship stands. | ## Example call ```json { "name": "update_client", "arguments": { "client_id": "d95d0d1e-99fb-4000-a76a-74a721165572" } } ``` ## What it returns ```json { "data": { "id": "9b1f2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d", "team_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Super Clean Laundry", "legal_name": "Super Clean Laundry (Pvt) Ltd", "website": "https://superclean.example.test", "industry": "Laundry services", "country": "LK", "timezone": "Asia/Colombo", "primary_contact_name": "Nadeesha Perera", "primary_contact_email": "nadeesha@example.test", "notes": null, "status": "active", "projects_count": 1, "archived_at": null, "created_at": "2026-10-28T09:12:00+00:00", "updated_at": "2026-10-28T10:40:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a country that is not a two-letter upper-case code, a status outside the four, or a name another client of the team already uses. - `RESOURCE_NOT_FOUND` — unknown client_id, or a client outside the token's scope. - `FORBIDDEN` — the token is a client guest's, or the holder's team role does not carry client:update. - `TOKEN_MISSING_ABILITY` — token lacks client:update. ## Example prompts > "The laundry shop's legal name is Super Clean Laundry (Pvt) Ltd; add their website too." > "Mark Beta Bank as active now that the project has started." > "Nadeesha's email is nadeesha@example.test; put it on the client." ## Related - [get_client](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-client) — the client as it stands. - [create_client](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/create-client) — a client that is not on the registry yet. - [Clients API](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients) — the same door over REST. --- # update_component Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-component Changes a component of a project in part; a field left out keeps its value and the tags replace the list when sent. Change a component by `component_id` with any of `name`, `slug`, `kind`, `description` (HTML), `tech_stack` (the list replaces the current one) and `owner_id`. A field left out keeps its value; a field sent as null is cleared. The change goes through the same action the Components page runs and `component.updated` goes out with what moved. A staff token with `component:update` on the project is needed. - Requires ability: `component:update` - Runs the same action as [`PATCH /v1/components/{component}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/components/update-a-component) - Fires events: [`component.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `component_id` | string | yes | UUID of the component. | | `name` | string | no | What the component is called within the project, 2 to 80 characters. | | `slug` | string | no | The slug shown in versions such as web@1.4.0. | | `kind` | `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure`, `other` | no | What the component is. | | `description` | string | no | What the component does, as HTML. | | `tech_stack` | array of string | no | Short technology tags; the list replaces the current one. | | `owner_id` | string | no | UUID of the team member who answers for it; null to clear. | ## Example call ```json { "name": "update_component", "arguments": { "component_id": "90828f38-f4fe-4c00-aeb5-f681a0715508" } } ``` ## What it returns ```json { "data": { "id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Web app", "slug": "web-app", "kind": "web_app", "description": "<p>The customer-facing site: services, ordering, tracking and bookings.</p>", "tech_stack": [ "Laravel 13", "Livewire 4", "Flux UI", "PostgreSQL", "Redis" ], "owner_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "position": 1, "environments_count": 3, "archived_at": null, "created_at": "2026-10-28T09:18:00+00:00", "updated_at": "2026-10-28T12:30:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a kind outside the list, a slug or name another component of the project already uses, or an owner_id who is not on the team. - `RESOURCE_NOT_FOUND` — unknown component_id, or a component outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry component:update. - `TOKEN_MISSING_ABILITY` — token lacks component:update. ## Example prompts > "Make Pat the owner of the web app component." > "Add Redis to the web app's stack." > "Rename the API component to Order API." ## Related - [get_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-component) — the component as it stands. - [delete_component](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-component) — take it away. - [Components API](https://alm-docs.labs.zuselab.dev/api/v1/reference/components) — the same door over REST. --- # update_design_reference Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-design-reference Changes a design reference of a project in part. Change a design reference by `design_reference_id` with any of `title`, `url`, `kind`, `referenceable_type` with `referenceable_id`, `version` and `notes` (HTML). A field left out keeps its value; a field sent as null is cleared. The change goes through the same action the Designs page runs and `design_reference.updated` goes out with what moved. A staff token with `design-reference:update` on the project is needed. - Requires ability: `design-reference:update` - Runs the same action as [`PATCH /v1/design-references/{reference}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references/update-a-design-reference) - Fires events: [`design_reference.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `design_reference_id` | string | yes | UUID of the design reference. | | `title` | string | no | What the reference is called, 2 to 160 characters. | | `url` | string | no | Where the design lives, as a full URL. | | `kind` | `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document`, `other` | no | What the link points at; other by default. | | `referenceable_type` | `project`, `component` | no | What the reference hangs off: project by default, or component. | | `referenceable_id` | string | no | UUID of the component when the target is a component. | | `version` | string | no | The version or revision of the design, as the designer labels it. | | `notes` | string | no | What to look at and why, as HTML. | ## Example call ```json { "name": "update_design_reference", "arguments": { "design_reference_id": "6ee4976b-499b-4400-af2c-112ee89091fd" } } ``` ## What it returns ```json { "data": { "id": "3b4c5d6e-7f8a-4b9c-8d0e-1f2a3b4c5d6e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "referenceable_type": "component", "referenceable_id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "kind": "figma", "title": "Order flow", "url": "https://www.figma.com/file/abc123/order-flow", "version": "v2", "notes": "<p>The payment step is now designed; the slot picker moved to its own screen.</p>", "added_by": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "position": 1, "created_at": "2026-10-28T09:40:00+00:00", "updated_at": "2026-10-29T10:05:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a url that is not an address, a kind or target outside its set, or a component target without a component of the project. - `RESOURCE_NOT_FOUND` — unknown design_reference_id, or a reference outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry design-reference:update. - `TOKEN_MISSING_ABILITY` — token lacks design-reference:update. ## Example prompts > "The order flow design is on v2 now; note that the payment step is designed." > "Move the ERD reference from the project to the API component." > "Change the prototype link to the new share address." ## Related - [get_design_reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-design-reference) — the reference as it stands. - [delete_design_reference](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-design-reference) — take it away. - [Design references API](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references) — the same door over REST. --- # update_environment Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-environment Changes an environment of a project in part; the health and the freeze have their own doors and never change here. Change an environment by `environment_id` with any of `name`, `kind`, `component_id`, `url`, `api_url`, `admin_url`, `branch`, `hosting`, `region`, `auto_deploy`, `protected`, `health_check_url`, `current_version` and `notes` (HTML). A field left out keeps its value; a field sent as null is cleared. The change goes through the same action the Environments page runs and `environment.updated` goes out with what moved. A staff token with `environment:update` on the project is needed. - Requires ability: `environment:update` - Runs the same action as [`PATCH /v1/environments/{environment}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments/update-an-environment) - Fires events: [`environment.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `environment_id` | string | yes | UUID of the environment. | | `name` | string | no | What the environment is called, 2 to 60 characters. | | `kind` | `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom` | no | What the environment is for. | | `component_id` | string | no | UUID of the component it runs; null when it serves the whole project. | | `url` | string | no | The address people open. | | `api_url` | string | no | The address the API answers on. | | `admin_url` | string | no | The address of the back office. | | `branch` | string | no | The branch deployed there. | | `hosting` | string | no | Where it runs, in words. | | `region` | string | no | The region it runs in. | | `auto_deploy` | boolean | no | Whether the pipeline deploys to it without a hand. | | `protected` | boolean | no | Whether a deployment needs a promotion approval. | | `health_check_url` | string | no | The address the scheduled health check calls. | | `current_version` | string | no | The version running there, as last recorded. | | `notes` | string | no | Anything the team should know, as HTML. | ## Example call ```json { "name": "update_environment", "arguments": { "environment_id": "7dc196f2-3d2f-4000-a1c0-910b57fc0c9f" } } ``` ## What it returns ```json { "data": { "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "component_id": "c0a1b2c3-d4e5-4f60-8a7b-9c0d1e2f3a4b", "name": "Staging", "kind": "staging", "url": "https://staging.example.test", "api_url": "https://api.staging.example.test", "admin_url": null, "branch": "develop", "hosting": "Managed cloud, Frankfurt", "region": "FSN1", "auto_deploy": true, "protected": true, "health": "healthy", "health_check_url": "https://staging.example.test/up", "last_checked_at": "2026-10-28T12:00:00+00:00", "consecutive_failures": 0, "current_version": "web@0.3.0", "frozen_until": null, "frozen": false, "notes": null, "position": 2, "created_at": "2026-10-28T09:20:00+00:00", "updated_at": "2026-10-28T12:05:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a kind outside the list, an address that is not a URL, a component_id that is not the project's, or a name already used for the same component. - `RESOURCE_NOT_FOUND` — unknown environment_id, or an environment outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry environment:update. - `TOKEN_MISSING_ABILITY` — token lacks environment:update. ## Example prompts > "Protect the staging environment and point its health check at /up." > "The UAT address changed to https://uat.example.test." > "Turn auto-deploy off on production." ## Related - [get_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-environment) — the environment as it stands, with its health. - [delete_environment](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/delete-environment) — take it away. - [Environments API](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments) — the same door over REST, with the freeze and the health verbs. --- # update_project_settings Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project-settings Changes the settings every later module reads, from the sprint length to the council's seats, after the person confirms. Change a project's settings by `project_id`: any of `sprint_length_days`, `working_days`, `timezone`, `estimation_unit`, `story_point_scale`, `default_assignee_rule`, `auto_transition_on_pr`, `auto_transition_on_merge`, `require_dor_for_sprint`, `require_dod_for_done`, `guest_can_comment`, `ai_assist_enabled`, `telegram_group_chat_id`, `wip_limits`, `hidden_columns`, `business_hours`, `auto_incident_on_down`, `council_voices` and `council_budget_percent`. A key left out keeps its value. The change goes through the same action the Settings page runs and `project.settings_changed` goes out with the keys that moved. The tool is destructive in the MCP sense: a setting changes how the project behaves rather than what it says, so a Heimdall turn pauses for the person's confirmation and an MCP client asks before sending. A staff token with `project:update` on the project is needed. - Requires ability: `project:update` - Runs the same action as [`PATCH /v1/projects/{project}/settings`](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings/update-a-projects-settings) - Fires events: [`project.settings_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-settings-changed) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `sprint_length_days` | integer | no | How long a sprint runs, 1 to 60 days. | | `working_days` | array of integer | no | The working days as ISO weekday numbers, Monday 1 to Sunday 7. | | `timezone` | string | no | The timezone the project's deadlines are read in, such as Asia/Colombo; null for the workspace default. | | `estimation_unit` | `points`, `hours` | no | What work is sized in. | | `story_point_scale` | `fibonacci`, `linear`, `tshirt` | no | The scale story points come from. | | `default_assignee_rule` | `reporter`, `lead`, `none` | no | Who a new item is handed to by default. | | `auto_transition_on_pr` | boolean | no | Whether an opened pull request moves its story into review. | | `auto_transition_on_merge` | boolean | no | Whether a merged pull request moves its story on. | | `require_dor_for_sprint` | boolean | no | Whether a story needs its definition of ready ticked before a sprint takes it. | | `require_dod_for_done` | boolean | no | Whether an item needs its definition of done ticked before it is done. | | `guest_can_comment` | boolean | no | Whether client guests may comment. | | `ai_assist_enabled` | boolean | no | Whether Heimdall assists on this project at all. | | `telegram_group_chat_id` | string | no | The Telegram group the project speaks in; null for none. | | `wip_limits` | object | no | A map of board column code to its work-in-progress limit. | | `hidden_columns` | array of string | no | The board columns the project hides. | | `business_hours` | object | no | An object with start and end as HH:MM, the hours SLAs count. | | `auto_incident_on_down` | boolean | no | Whether a production environment found down twice opens an incident. | | `council_voices` | object | no | A map of council moment code to the voice keys that sit on it. | | `council_budget_percent` | integer | no | The share of the team's monthly AI budget the council may spend here, 1 to 100. | ## Example call ```json { "name": "update_project_settings", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "sprint_length_days": 7, "working_days": [1, 2, 3, 4, 5], "timezone": "Asia/Colombo", "estimation_unit": "points", "story_point_scale": "fibonacci", "default_assignee_rule": "nobody", "auto_transition_on_pr": true, "auto_transition_on_merge": true, "require_dor_for_sprint": false, "require_dod_for_done": true, "guest_can_comment": true, "ai_assist_enabled": true, "telegram_group_chat_id": null, "wip_limits": { "in_progress": 4 }, "hidden_columns": [], "business_hours": { "start": "09:00", "end": "18:00" }, "auto_incident_on_down": true, "council_voices": { "intake_scaffold": ["odin", "thor", "loki", "tyr", "frigg", "baldur"] }, "council_budget_percent": 25 } } ``` ## How it fails - `VALIDATION_FAILED` — a sprint length outside 1 to 60, a weekday outside 1 to 7, an unknown timezone, a unit, scale or rule outside its set, a WIP limit on an unknown column, business hours without both ends, a council voice that does not exist, or a budget share outside 1 to 100. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry project:update. - `TOKEN_MISSING_ABILITY` — token lacks project:update. ## Example prompts > "Run one-week sprints on the laundry project and let the client comment." > "Cap work in progress at four items on SCL." > "Take Hel off the intake council for this project." ## Related - [get_project_settings](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project-settings) — the settings as they stand. - [update_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project) — the project's own record. - [Project settings API](https://alm-docs.labs.zuselab.dev/api/v1/reference/project-settings) — the same door over REST. --- # update_project Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project Changes a project's record in part, so a conversation can add the launch date, the leads, the client or a status one answer at a time. Change a project by `project_id` with any of `name`, `key` (only while nothing is numbered under the project), `client_id`, `tagline`, `description` (HTML), `status` (every status but `archived`, which has its own door in the application), `methodology`, `visibility`, `lead_pm_id`, `lead_ba_id`, `lead_dev_id`, `lead_qa_id`, `starts_on`, `target_launch_on`, `launched_on` and `budget_hours`. A field left out keeps its value; a field sent as null is cleared. The change goes through the same action the registry page runs and `project.updated` goes out with what moved. The project's settings, from the sprint length to the council's seats, change through `update_project_settings`. A staff token with `project:update` on the project is needed. - Requires ability: `project:update` - Runs the same action as [`PATCH /v1/projects/{project}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects/update-a-project) - Fires events: [`project.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-updated) - Annotations: Destructive ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | UUID of the project. | | `name` | string | no | The project's name, 3 to 100 characters, unique within the team. | | `key` | string | no | Two to six upper-case letters and digits starting with a letter; changeable only while nothing is numbered under the project. | | `client_id` | string | no | UUID of the client; null to make the project internal. | | `tagline` | string | no | One line about the project, up to 160 characters. | | `description` | string | no | What the project is, as HTML. | | `status` | `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` | no | The lifecycle status. | | `methodology` | `scrum`, `kanban`, `hybrid`, `waterfall` | no | How the project is run. | | `visibility` | `team`, `members_only` | no | team or members_only. | | `lead_pm_id` | string | no | UUID of the project manager; null to leave the seat empty. | | `lead_ba_id` | string | no | UUID of the lead business analyst; null to leave the seat empty. | | `lead_dev_id` | string | no | UUID of the lead developer; null to leave the seat empty. | | `lead_qa_id` | string | no | UUID of the QA lead; null to leave the seat empty. | | `starts_on` | string | no | The planned start, YYYY-MM-DD. | | `target_launch_on` | string | no | The target launch, YYYY-MM-DD. | | `launched_on` | string | no | The day the project went live, YYYY-MM-DD. | | `budget_hours` | integer | no | The budgeted effort in whole hours. | ## Example call ```json { "name": "update_project", "arguments": { "project_id": "308f7cfe-03db-4a00-a514-7fab9fdf89e9" } } ``` ## What it returns ```json { "data": { "id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "team_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "client_id": "9b1f2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d", "key": "SCL", "name": "Super Clean Laundry", "tagline": "Laundry orders online", "description": "<p>An informational website with laundry order placement, tracking, payment and appointment scheduling.</p>", "status": "planning", "methodology": "kanban", "visibility": "team", "health": "on_track", "lead_pm_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "lead_ba_id": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "lead_dev_id": null, "lead_qa_id": null, "starts_on": "2026-11-02", "target_launch_on": "2027-02-27", "launched_on": null, "budget_hours": 1200, "archived_at": null, "created_at": "2026-10-28T09:15:00+00:00", "updated_at": "2026-10-28T11:02:00+00:00" } } ``` ## How it fails - `VALIDATION_FAILED` — a field outside its limits, a key change once a record carries the old one, a name or key the team already uses, a lead who is not on the team, a client_id that is not the team's, or status set to archived. - `RESOURCE_NOT_FOUND` — unknown project_id, or a project outside the token's scope or the person's visibility. - `FORBIDDEN` — the token is a client guest's, or the holder's role on the project does not carry project:update. - `TOKEN_MISSING_ABILITY` — token lacks project:update. ## Example prompts > "Set the target launch for the laundry project to the end of February and make Pat the project manager." > "Move SCL into planning and run it as Kanban." > "Give the Customer Portal a budget of 1,200 hours." ## Related - [get_project](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/get-project) — the project as it stands. - [update_project_settings](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/project/update-project-settings) — the knobs every module reads. - [Projects API](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects) — the same door over REST. --- # Role & Group Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups A role is the permission set one member holds, seeded per team and editable by its owner; a group is a bundle of members with permissions they share. These tools read both, with permissions written as the same ability strings a token carries. ## Tools - [`list_groups`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-groups) — List Groups (read) - [`list_roles`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-roles) — List Roles (read) --- # list_groups Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-groups List groups across every team the caller owns or belongs to, with the permissions each group shares and the members in it. List groups across every team the caller owns or belongs to. A group bundles members who share a set of permissions on top of their roles; each row carries those `permissions` and the `member_ids` in it. Read-only; groups are edited in the application. - Requires ability: `group:view-any` - Runs the same action as [`GET /v1/groups`](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups/list-groups) - Annotations: Read-only ## Arguments This tool takes no arguments. ## Example call ```json { "name": "list_groups", "arguments": {} } ``` ## What it returns ```json { "data": [ { "id": "1f68d92a-04c5-4e83-97b1-3d6a05e2f847", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "code": "release-crew", "name": "Release crew", "permissions": ["webhook-endpoint:view-any", "webhook-delivery:view-any"], "member_ids": ["5e0c7a2d-9b1f-4c3e-8d6a-2f4b7c9e1a30"] } ], "meta": { "total": 1 } } ``` ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks group:view-any. ## Example prompts > "List all groups across my teams." > "Who is in the release crew group, and what does it grant?" ## Related - [list_roles](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-roles) - [list_team_members](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members/list-team-members) - [Groups API](https://alm-docs.labs.zuselab.dev/api/v1/reference/groups) - [Groups](https://alm-docs.labs.zuselab.dev/workspace/groups) --- # list_roles Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-roles List roles across every team the caller owns or belongs to. Each role carries a flat permission list and whether it was seeded. List roles across every team the caller owns or belongs to. Each role carries a flat `permissions` array, the same `entity:action` strings a token would use, and `seeded`, which marks the eight roles every team starts with (`admin`, `project_manager`, `business_analyst`, `developer`, `qa_engineer`, `release_manager`, `viewer`, `guest`) apart from the ones the owner added. Read-only; roles are edited in the application. - Requires ability: `role:view-any` - Runs the same action as [`GET /v1/roles`](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles/list-roles) - Annotations: Read-only ## Arguments This tool takes no arguments. ## Example call ```json { "name": "list_roles", "arguments": {} } ``` ## What it returns ```json { "data": [ { "id": "d05e1a83-7c46-4f29-b613-8ae407c9d251", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "code": "developer", "name": "Developer", "description": "Builds against the backlog and the board, reads the planning record and executes tests.", "seeded": true, "permissions": ["project:view-any", "project:view", "activity:read"] } ], "meta": { "total": 8 } } ``` ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks role:view-any. ## Example prompts > "Which roles exist across my teams and what can each of them do?" > "Show me the permission list of the Business Analyst role." ## Related - [list_groups](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-groups) - [list_team_members](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members/list-team-members) - [Roles API](https://alm-docs.labs.zuselab.dev/api/v1/reference/roles) - [Roles](https://alm-docs.labs.zuselab.dev/workspace/roles) --- # Team Member Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members Team members are the staff and guests who collaborate inside a team, each with a role that decides what they may do. These tools list the roster, the owner first, with email addresses only for a token that may view a member. ## Tools - [`list_team_members`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members/list-team-members) — List Team Members (read) --- # list_team_members Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members/list-team-members List the people on a team, the owner first, each with the role they hold there. Email addresses only for a token that may view a member. List the staff and guests on a team. The owner's pseudo-row is prepended to every member, with `is_owner: true`, a null `role_id` and the role code `admin`; every other row carries the role the member holds on this team, such as `business_analyst` or `developer`, which decides what they may do there. A token holding only `team-member:view-any` gets the roster without email addresses: the `email` key is absent, not null. A token that also holds `team-member:view` gets it on every row, as the REST show route does. - Requires ability: `team-member:view-any` - Runs the same action as [`GET /v1/teams/{team}/members`](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members/list-a-teams-members) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `team_id` | string | yes | UUID of the team whose members to list. | ## Example call ```json { "name": "list_team_members", "arguments": { "team_id": "ffaf93ac-a725-4800-a198-ea154bdce1f4" } } ``` ## What it returns ```json { "data": [ { "id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "name": "Priya Patel", "email": "priya@example.com", "role_id": null, "role": "admin", "is_owner": true, "joined_at": "2026-01-12T10:05:00+00:00" }, { "id": "5e0c7a2d-9b1f-4c3e-8d6a-2f4b7c9e1a30", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "name": "Nimal Perera", "email": "nimal@example.com", "role_id": "d05e1a83-7c46-4f29-b613-8ae407c9d251", "role": "developer", "is_owner": false, "joined_at": "2026-04-02T14:15:00+00:00" } ], "meta": { "total": 2 } } ``` ## How it fails - `RESOURCE_NOT_FOUND` — team_id is not a valid UUID, or the team falls outside the caller's visibility set. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks team-member:view-any. ## Example prompts > "Who is on team `a83f0d51-4c92-4b7e-8615-2fd9e70a3c86`?" > "List the developers on my team." > "Which guests have been invited into the team?" ## Related - [get_team](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/get-team) - [list_teams](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/list-teams) - [list_roles](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/roles-and-groups/list-roles) — resolve role_id to permission sets. - [Team Members API](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members) - [Members and guests](https://alm-docs.labs.zuselab.dev/workspace/members-and-guests) --- # get_team Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/get-team Fetch a single team by UUID from the caller's visibility set (owned or joined). A foreign team answers RESOURCE_NOT_FOUND. Fetch one team the authenticated person owns or belongs to. A team they are not part of always answers `RESOURCE_NOT_FOUND`, never a permission error, so existence does not leak. - Requires ability: `team:view` - Runs the same action as [`GET /v1/teams/{team}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/get-a-team) - Annotations: Read-only ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `team_id` | string | yes | UUID of the team to fetch. | ## Example call ```json { "name": "get_team", "arguments": { "team_id": "ffaf93ac-a725-4800-a198-ea154bdce1f4" } } ``` ## What it returns ```json { "data": { "id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "name": "Zuse Technologies", "owner_user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "created_at": "2026-01-12T10:05:00+00:00" }, "meta": {} } ``` ## How it fails - `RESOURCE_NOT_FOUND` — team_id is not a valid UUID, or it falls outside the caller's visibility set. - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks team:view. ## Example prompts > "Show me team `a83f0d51-4c92-4b7e-8615-2fd9e70a3c86`." > "Who owns this team?" ## Related - [list_teams](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/list-teams) - [list_team_members](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members/list-team-members) - [Teams API](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams) --- # Team Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team A team groups the people, roles and projects of one organisation, and every token is scoped to exactly one. These tools read the teams the account can act in; changing a team stays in the application and the REST API. ## Tools - [`get_team`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/get-team) — Get Team (read) - [`list_teams`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/list-teams) — List Teams (read) --- # list_teams Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/list-teams List every team the authenticated person owns or belongs to. Read-only — changing a team stays in the application and the REST API. List every team the caller has visibility into. Useful before minting a token scoped to a specific team: an agent can surface a picker instead of making the person hunt for a UUID. Read-only; creating, renaming and deleting a team stays in the application and the REST API. - Requires ability: `team:view-any` - Runs the same action as [`GET /v1/teams`](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams/list-teams) - Annotations: Read-only ## Arguments This tool takes no arguments. ## Example call ```json { "name": "list_teams", "arguments": {} } ``` ## What it returns ```json { "data": [ { "id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "name": "Zuse Technologies", "owner_user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80", "created_at": "2026-01-12T10:05:00+00:00" } ], "meta": { "total": 1 } } ``` ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks team:view-any. ## Example prompts > "Which teams do I belong to?" > "List my teams so I know where to scope my next API token." ## Related - [get_team](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team/get-team) - [list_team_members](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/team-members/list-team-members) - [Teams API](https://alm-docs.labs.zuselab.dev/api/v1/reference/teams) - [Teams](https://alm-docs.labs.zuselab.dev/workspace/teams) --- # Token Tools Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens Personal access tokens authorise every REST and MCP call, and the plaintext value exists exactly once. These tools list the token holder's own tokens and revoke one, so an agent can retire a credential it was given without a person opening the application; minting stays in the application. ## Tools - [`list_tokens`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/list-tokens) — List Personal Access Tokens (read) - [`revoke_token`](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/revoke-token) — Revoke Personal Access Token (destructive) --- # list_tokens Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/list-tokens List the authenticated person's own personal access tokens with abilities and scope tuples split out. The plaintext value is never shown. List the authenticated person's own personal access tokens with abilities and scope tuples split out. `scope:team:...` and `scope:project:...` entries are peeled off the raw abilities array into a structured `scopes` object for readability, and `abilities_count` counts the gate abilities alone. The plaintext token value is never surfaced; minting happens under **Account Settings → API Tokens**. - Requires ability: `token:view-any` - Runs the same action as [`GET /v1/tokens`](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/list-the-callers-tokens) - Annotations: Read-only ## Arguments This tool takes no arguments. ## Example call ```json { "name": "list_tokens", "arguments": {} } ``` ## What it returns ```json { "data": [ { "id": "1284", "name": "Cursor on the CRM repository", "abilities": ["project:view-any", "project:view", "activity:read"], "scopes": { "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "project_ids": ["7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13"] }, "abilities_count": 3, "last_used_at": "2026-10-26T09:15:00+00:00", "expires_at": null, "created_at": "2026-10-01T10:05:00+00:00" } ], "meta": { "total": 1 } } ``` A token's `id` is an integer serialised as a string, the one exception to the UUID rule every other id follows. ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `TOKEN_MISSING_ABILITY` — token lacks token:view-any. ## Example prompts > "Show my active API tokens." > "Which abilities does each of my tokens carry, and which projects are they confined to?" ## Related - [revoke_token](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/revoke-token) - [Tokens API](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens) - [API tokens](https://alm-docs.labs.zuselab.dev/workspace/api-tokens) --- # revoke_token Source: https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/revoke-token Revoke one of the authenticated person's own tokens by id. Self-revocation is refused. Revoke one of the authenticated person's own personal access tokens by id. Refuses self-revocation: the token authenticating this call cannot delete its own row. Revoke the current token under **Account Settings → API Tokens** or from a different token instead. Revocation is immediate; any client still holding the token gets `AUTHENTICATION_REQUIRED` on its next call. Idempotent: an id already revoked answers `RESOURCE_NOT_FOUND`, exactly as an id that was never yours does. - Requires ability: `token:delete` - Runs the same action as [`DELETE /v1/tokens/{token}`](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens/revoke-a-token) - Fires events: [`token.revoked`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token/token-revoked) - Annotations: Destructive, Idempotent ## Arguments | Field | Type | Required | Description | | --- | --- | --- | --- | | `token_id` | string | yes | Id of the token to revoke, as `list_tokens` reports it. Must belong to the authenticated person and cannot be the token authenticating this call. | ## Example call ```json { "name": "revoke_token", "arguments": { "token_id": "f18ef91a-282d-4800-afb9-80a323759096" } } ``` ## What it returns ```json { "data": { "token_id": "1284", "revoked": true }, "meta": {} } ``` ## How it fails - `AUTHENTICATION_REQUIRED` — no authenticated user on the request. - `VALIDATION_FAILED` — token_id is empty, or the caller tried to revoke the token making the call. - `RESOURCE_NOT_FOUND` — the token does not belong to the authenticated person, or was already revoked. - `TOKEN_MISSING_ABILITY` — token lacks token:delete. ## Example prompts > "Revoke my 'Cursor on the CRM repository' token." (confirm first)" > "Delete token `1284`." ## Related - [list_tokens](https://alm-docs.labs.zuselab.dev/mcp/v1/tools/tokens/list-tokens) - [Tokens API](https://alm-docs.labs.zuselab.dev/api/v1/reference/tokens) - [Leaked-credential playbook](https://alm-docs.labs.zuselab.dev/mcp/v1/security#leaked-credential-playbook) --- # MCP Troubleshooting Source: https://alm-docs.labs.zuselab.dev/mcp/v1/troubleshooting ## Client can't connect - **"Server unreachable"**: confirm the server address is reachable from the client. Opening it in a browser answers a sign-in challenge, not a 404. - **TLS errors**: update the client. Older releases of Claude Desktop and Cursor do not negotiate MCP streaming HTTP correctly. - **Proxy in the way**: corporate proxies sometimes strip the `Authorization` header. Test over a direct network first. ## Sign-in never finishes - The server URL must be exactly the address you were given. A trailing slash or a path makes the client compare it to the address the server publishes and refuse the connection before you see a sign-in page. - You landed on the Zuse Bifrost sign-in page instead of the consent screen: sign in (two-factor authentication and passkeys apply), and the client retries the authorization on its own. - The consent screen appeared but the client still says it is not connected: press **Authorize** on it; **Cancel** sends the client an error and it stays disconnected. - Corporate proxies that rewrite or block the application's `/.well-known/…` documents break discovery. Test over a direct network first. ## Tool list is empty - Client config JSON invalid. Validate with `jq` or similar. - Restart required after editing config. MCP servers load at client launch. - The tool list itself is the same for every credential; a token missing an ability still sees the tool and is refused when it calls it, with the ability named. ## Specific tool is refused With a personal access token the error envelope includes `required_ability` under `error.context`. Mint a new token with that ability added, replace the old one in the client config, restart the client. An OAuth connection carries every ability your role grants, so a refusal there means your team role lacks the permission itself; ask the team owner to grant it. ## Specific tool answers not found - Tenant mismatch: the token's `scope:team:` or `scope:project:` does not include the project, team or endpoint you referenced. Out-of-scope data is reported as absent rather than forbidden, so existence does not leak. - Entity does not exist. Double-check the id. ## Tool call times out - Short tool calls that time out usually indicate a transient slowdown; retry after a brief backoff. - A webhook test or a replay queues work rather than waiting for it: [`test_webhook_endpoint`](/mcp/v1/tools/observability/test-webhook-endpoint) answers with the pending delivery, which [`get_webhook_delivery`](/mcp/v1/tools/observability/get-webhook-delivery) reads once the worker has posted it. ## Rate limited (429) - Authenticated calls are limited to 120 a minute per token. Claude Desktop and Cursor pace tool calls internally, so hitting it usually means a tight loop. - Unauthenticated traffic on the MCP endpoint is bucketed separately at 5 a minute per address. If a client is hitting `RATE_LIMITED` after only a handful of attempts, the token is missing or malformed; fix the `Authorization` header before retrying. - The `Retry-After` header tells you how long to wait. Honour it. ## Agent loops asking for confirmation - The client is auto-denying write tools. Toggle auto-approve for the zusegenius namespace in the client's settings. - If you want confirmation on every write, that is the default; the server marks every tool that changes data as destructive and leaves the confirmation to the client. ## "Session expired" or 401 after hours of chat - An OAuth access token lasts an hour and the client renews it silently. If it stopped renewing (the refresh token was discarded, or 30 days passed with no use), disconnect and connect again inside the client. - Personal access tokens do not expire mid-session unless you revoked them. - The session-level timeout inside some clients caps at a few hours for safety; reconnect. ## Related - [MCP security](/mcp/v1/security) - [Error envelope](/api/v1/errors) --- # Acceptance Criteria Source: https://alm-docs.labs.zuselab.dev/planning/acceptance-criteria By the end of this page you will have written the acceptance criteria of a story as rows a tester can tick, know what each row carries and what reads it, and know how the rows are ordered, replaced and kept with every version of the item. An **acceptance criterion** is one sentence that says when a story is done: _a member who crosses the platinum threshold gets the email within the hour_. A story carries as many as it needs, each with an optional **Given**, **When** and **Then** for the ones worth spelling out as a scenario, and a **kind** that says what the row checks: the happy path, what must be refused, the edge of the input or a quality the story must keep. The rows are structured, not free text, so QA can tick them one by one from a passing run, test cases can be drafted one per row, and the traceability matrix can say which criterion a test covers. ## Before you begin | You want to | You need | | -------------------------------- | ------------------------------------------------------------------------------------- | | Read the criteria | `work-item:view-any` on the project. A client guest reads the agreed items' criteria. | | Write or change the criteria | `work-item:update`, or `work-item:create` while adding the item. | | Replace them through the API | `work-item:update` on a token. | The rows live on the item: open the project, choose **Backlog** in the sidebar and open the item's editor. Every kind of item may carry criteria; a story almost always should. ## Write the criteria [step] 1. Open the item in the editor (**New Item**, or **Edit Item** on an existing one) and scroll to **Acceptance Criteria**. 2. Press **Add Criterion**. A row appears with its **kind**, its sentence and the three scenario fields. 3. Type the sentence: one line a tester can tick, in the persona's terms, with the outcome in it. 4. Pick the **kind** when it is not the happy path: _negative_ for what must be refused, _edge case_ for the boundary of the input, _non-functional_ for a quality the story must keep. 5. Fill **Given**, **When** and **Then** when the scenario is worth spelling out: the starting state, what happens and what must be true after. Leave them empty when the sentence is enough. 6. Add the next row, and press **Create Item** or **Save Changes** when the list is complete. The rows are saved with the item in one version. A row with nothing in its sentence is refused; a sentence needs at least three characters and at most 300, each scenario field at most 500, and an item carries at most fifty rows. <Callout type="info" title="Write the negative and the edge"> A story with only happy-path criteria is half a story. Heimdall's quality review will flag a story without a negative criterion, and the test generator drafts one case per kind, so a row of each kind gives QA the suite it needs. </Callout> ## Order and remove rows [step] Each row carries three buttons: **Move up** and **Move down** change its place in the list, and **Remove criterion** drops it. The order is the order QA ticks them in and the order the test generator drafts them in, so put the main path first. Removing a row takes effect when you save; the version before the save still carries it, so a row removed by mistake comes back with **Restore this version**. ## Read the criteria [step] The item's detail flyout lists the rows under **Acceptance Criteria** in order, each with its kind badge, its sentence and its scenario on one line beneath. An item without rows reads _No criteria yet_. A client guest reads the rows of the agreed items like everyone else; they are what the client signs off. ## Replace the criteria over the API Beside the editor, a token replaces an item's criteria in one call: `PUT /v1/work-items/{item}/acceptance-criteria` takes the full list in order, keeps and changes the rows sent with an `id`, adds the rows without one, removes every row left out, and answers the item with its rows. The call writes a version of the item and `work_item.criteria_changed` goes out with how many rows were added, changed and removed; a call that changes nothing stays quiet. The same rows can be sent inside `POST …/work-items` and `PATCH /v1/work-items/{item}` under `acceptance_criteria`, which saves them with the item's fields in one version. ## All the fields <Accordions> <Accordion title="Sentence"> Required, 3 to 300 characters. The criterion in one line a tester can tick, with the outcome in it. Shown in the flyout, sent as `title` over the API and drafted into a test case's title. </Accordion> <Accordion title="Given, When and Then"> Optional, up to 500 characters each. The scenario behind the sentence: the starting state, what happens and what must be true after. Written in the Gherkin form so a test generator and an automation suite read them straight; empty when the sentence stands alone. </Accordion> <Accordion title="Kind"> Required, _functional_ by default. Functional (the happy path), negative (what must be refused or must not happen), edge case (the boundary of the input) or non-functional (a quality the story must keep: speed, access, accessibility). The [status reference](/reference/status-reference#criterion-kind) lists them; the test generator drafts one case per kind. </Accordion> <Accordion title="Position, verified at and verified by"> Kept by the application. The position is the row's place in the list, set by the order you save; verified is stamped when QA ticks the row from a passing run or by hand, once the test module lands. </Accordion> </Accordions> ## What happens next - The rows travel inside every [version](/planning/revisions-and-versions) of the item, so a restore brings the criteria of that version back with the fields. - A save of the item that changes the rows writes a version whose diff lists `acceptance_criteria`; a replace through the API also raises `work_item.criteria_changed`. - Test cases will be drafted one per row, QA will tick each row from an execution, and the traceability matrix will show which test covers which criterion, as the test module lands. ## Heimdall on this page Heimdall reads an item's criteria through the tools the MCP server offers and writes them into the stories it drafts. Try: “Which stories under the loyalty tiers epic have no negative criterion?” · “Draft a story for the tier downgrade warning with a functional, a negative and an edge criterion.” · “Read the criteria of CRM-142 and tell me what a tester would still have to ask.” Turning a sentence into a Given, When and Then, and suggesting the rows a story lacks, arrive with the inline assists. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ----------------------------------- | ------------------------------------------------------------- | | Read the criteria | `work-item:view-any`, `work-item:view` | | Write them with the item | `work-item:create` on a new item, `work-item:update` after | | Replace them through their own verb | `work-item:update` | ## Frequently asked <Accordions> <Accordion title="Do tasks and bugs carry criteria too?"> They may. The rows are on every kind, and a bug's _expected result_ often reads best as a criterion a tester ticks. A story is the kind that should never be without them. </Accordion> <Accordion title="Why is there no tick box yet?"> Ticking a row is QA's act, from a passing execution or by hand, and lands with the test module; the verified stamps are already on the row for it. </Accordion> <Accordion title="I replaced the rows and the item's version count did not change. Why?"> A replace that sends the rows exactly as they are changes nothing, so nothing is written and nothing is announced. Change a sentence, a scenario, a kind or the order and a version follows. </Accordion> </Accordions> ## Related - [Backlog](/planning/backlog) — the items the criteria belong to - [Revisions and versions](/planning/revisions-and-versions) — the criteria inside every version - [Status reference](/reference/status-reference#criterion-kind) — the four kinds - [`work_item.*` webhook events](/webhooks/v1/events/work-item) --- # Approvals Source: https://alm-docs.labs.zuselab.dev/planning/approvals By the end of this page you will know how a planning record becomes approved: who submits it, who the project's policy names to decide, what each verdict does, how a request is resubmitted after changes or withdrawn, where the inbox is, what the clocks do, how the policies are set, and what a client guest sees and signs. An approval is a promise about a record: that the people the project named read this version and agreed to it. Zuse Bifrost keeps that promise literal. Submitting binds a request to the version of the record as it stood; every verdict is written once per person per round with a signature hash; carrying the request stamps the record approved at the next version, bound to the revision that was read. Nothing reaches **Approved** by hand any more: the status menu offers only the moves that are not the approval engine's. ## Before you begin | To | You need | | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Submit, resubmit or withdraw your own | The record's update permission (`requirement:update`, `epic:update` and so on), as staff | | Decide a request | To be named on it by the project's policy, and to reach the project | | Withdraw someone else's request | `approval:withdraw` | | Read the inbox and a project's approvals | `approval:view-any`; a client guest reads only the requests that name them | | Change a project's policies | `approval:manage-policies` | | Submit a baseline, or ask the client to sign a scope baseline | `baseline:freeze`, as staff | | Submit a change request | `change-request:approve`, as staff | Approvals are on by default: every project starts with the workspace defaults for each kind, and a submission on a project nobody configured asks the project managers. Six kinds are open today: the **Requirement review**, the **Baseline approval**, the **Client sign-off** on a scope baseline, the **Change request**, the **Document approval** and the **Sprint commitment**; the other kinds are declared, documented below, and open with their modules. ## What can be approved The requirement review covers the five planning records that carry versions: an [epic](/planning/epics), a story on the [backlog](/planning/backlog), a [requirement](/planning/functional-requirements), a [business rule](/planning/business-rules) and a [use case](/planning/use-cases). Each shows the **Approval** bar in its flyout, under the action buttons. A story keeps its board status throughout; the other four move into **In review** on submission and into **Approved** when the request carries. A [baseline](/planning/baselines) passes two gates on the same bar: the **Baseline approval** that freezes it and locks every record it holds, then, for a scope baseline, the **Client sign-off** by the stakeholders flagged as approving requirements, resolved from their seats rather than from the policy's lists. A [change request](/planning/change-requests) passes the **Change request** gate, whose approval unlocks the baseline items it names; the stakeholder who asked for the change is added to its approvers beside the policy's people. A [generated document](/documents/generated-documents) passes the **Document approval**: a rendered draft BRD or SRS is submitted from its flyout once it has a file, the project manager role answers by default, and the approval makes the copy **Final**, stamps its version and supersedes the kind's earlier final copy on the project, which is when the client's guests see it; a rejection returns it to **Draft** for the author to edit and render again. A [sprint](/building/sprints) passes the **Sprint commitment**: a planned sprint with its dates and its scope is submitted from its flyout, the project manager and the developer roles answer by default under the **Any one approver** rule, and the approval starts the sprint, stamping the points its scope held; a rejection returns it to **Planned**. Only one sprint of a project runs at a time, so a sprint cannot be submitted while another runs. | Kind | Code | What it decides | Open | | ------------------------ | ---------------------- | -------------------------------------------------------------------------------- | ----- | | **Requirement review** | `requirement_review` | Epics, stories, requirements, rules and use cases before they count as approved. | Now | | **Client sign-off** | `client_sign_off` | The client's word on a scope baseline or a UAT run. | Now | | **Test plan approval** | `test_plan_approval` | A test plan before its runs may start. | Later | | **Baseline approval** | `baseline_approval` | A baseline before its items are frozen. | Now | | **Change request** | `change_request` | A change request before it unlocks the baseline it names. | Now | | **Deployment promotion** | `deployment_promotion` | A deployment into a protected environment. | Later | | **Release go-live** | `release_go_live` | A release before it goes to production. | Later | | **Post-mortem sign-off** | `post_mortem_sign_off` | An incident's post-mortem before it is final. | Later | | **Document approval** | `document_approval` | A generated document before it is final and shared. | Now | | **Timesheet approval** | `timesheet_approval` | A member's week of time logs before it is locked. | Later | | **Sprint commitment** | `sprint_commitment` | The team's commitment when a sprint starts. | Now | ## Submit a record for review [step] Open the record's flyout. The **Approval** bar reads _Not submitted for approval yet._ and offers **Submit for review** (or **Submit again** once it carries a version) to someone who may change the record; a draft and an approved record that changed since can be submitted, a record already under review cannot. Press it, write a few lines for the approvers under **Summary** (what changed and why; they read it before the record) and press **Submit for review** again. What happens on submission: - The record's latest version becomes the version under review. - Where the record has a review status, it moves into **In review**; a story keeps its board status. - The approvers are resolved from the project's policy for the kind at that moment: everyone holding one of the named project roles (a membership on the project counts first, the team role on a whole-team project otherwise), the people named by name, and the stakeholder seats with a linked guest account. The list is written onto the request and does not move afterwards. - Each approver is told through their alert destinations, with a button that opens the approval in the inbox. The person who submitted hears nothing; they raised it. - The policy's clocks start: when the request is due, when the undecided approvers are nudged, when the request may carry by itself. The bar then reads _Waiting on 0 of 1 approvals; due in 3 days._, lists the approvers as badges that fill in as they answer, and shows your summary. A submission on a project whose gate is switched off is approved at once and says so. <Callout type="warn" title="A gate that names nobody"> A submission is refused when the policy is on and resolves to no one, for example a project with no project manager and nobody named. The message says so; name an approver under the project's settings or switch the gate off. </Callout> ## Decide [step] An approver sees three buttons on the bar, in the record's flyout or in the inbox: **Approve**, **Request changes** and **Reject**. Each opens a short confirmation with a **Reason**; the reason is required for a rejection or a request for changes when the project's policy says so, which it does by default, and optional otherwise. You decide once per round. | Verdict | What it does | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Approve** | Counts towards what the rule needs. When enough approvers agree, the request carries: the record is approved at the next version, stamped with when and with the revision that was read. | | **Request changes** | Holds the request. Nobody else decides until the author edits the record and resubmits it as the next round; the verdicts of the earlier round stay on the record. | | **Reject** | Closes the request under every rule. The record returns to where it stood: draft when it was never approved, approved at its last version when it was. | The policy's rule says how many approvals carry a request: **Any one approver**, **Every approver**, or **A quorum** of a set size. The bar shows the tally the whole time, and the person who raised the request hears about every verdict: an approval that does not yet carry it says how many are still needed. ## Resubmit after changes [step] When an approver asked for changes, the bar reads who asked and on which round and offers **Resubmit** to someone who may change the record. Edit the record first; then press **Resubmit**, add new words for the approvers or keep the earlier ones, and confirm. The request opens the next round: pending again, the approvers resolved again from the policy, the clocks reset, the earlier verdicts kept below the line. The record never left review in between. ## Withdraw [step] **Withdraw** takes an open request back. The person who raised it may always do so, and so may anyone whose role on the project carries `approval:withdraw`; a client guest never may. The record returns to where it stood and the request leaves the approvers' queues without a word to them. A new submission later is a new request. ## When an approved record changes Editing an approved record does not take its approval away: the record stays approved at its version, and the bar shows **Changed since** with _submit it again to approve the new version_. A new submission opens a new request; its approval stamps the next version, and a rejection returns the record to approved at the version it had. Readers who must see only what was agreed, such as a client guest, see the record by its status, which an edit leaves as it is. ## The inbox [step] **Approvals** in the sidebar, with a badge of what waits on you, opens the inbox: every request on the projects you can see, in three views: - **Waiting on me**: the pending requests that name you and that you have not answered this round. - **Raised by me**: everything you submitted, open or settled. - **All**: everything you may read. Filter by project, status and the record's family, or search by reference or title; sort by reference, title, kind, status or due date. Open a row for the flyout: the record's reference and title, who raised it and when, **Open** followed by its reference to its own page with the flyout showing, and the same approval bar as the record carries, so a verdict is taken here without leaving the inbox. A row past its due date shows the time in red. ## The clocks Every policy sets three optional clocks, in hours from the submission: - **Hours to decide**: when it runs out, the request is overdue, the project's lead (or the team's owner when the project names none) is told once, and `approval.escalated` goes out. The request stays pending; the approvers may still decide. - **Reminder after**: the approvers who have not answered are nudged once. - **Approve by itself after**: an unanswered request carries on its own, announced as an approval with `auto` set, and the author is told the time ran out. The clocks are read every hour. A resubmission resets them for the new round. ## Set the policies [step] Under the project's **Settings**, the **Approvals** section holds one editor per open kind. Each starts from the workspace defaults and says so with a **Workspace default** badge until the project saves its own, which turns it into **Set on this project**. A person without `approval:manage-policies` sees a read-only summary. - **Rule** and **Quorum**: how many approvals carry a request; the quorum applies to the quorum rule alone. - **Approver roles**: tick the project roles whose holders approve. The guest role is never offered; the client's people are named through their seats. - **Named people**: people asked whatever their role; each must be a member of the team. - **Stakeholder seats**: the client's stakeholders; a seat with no linked guest account is listed for the record and cannot decide. - **Hours to decide**, **Reminder after**, **Approve by itself after**: the clocks, 1 to 720 hours each; empty for none. - **Reason required on a refusal** and **Gate on**. A line under the fields says who the policy asks today, resolved live as you change it, or warns that nobody matches. **Save Policy** writes it; requests already open keep the approvers they were sent to. **Fill with Defaults** puts the workspace defaults back into the form without saving. ## All the fields <Accordions> <Accordion title="Summary (ApprovalSubmitForm.summary)"> What changed and why, for the approvers, up to 2000 characters; optional. Shown as a quote under the approvers on the bar and in the inbox, and quoted in the approvers' alert. On a resubmission, new words replace the earlier ones and empty words keep them. </Accordion> <Accordion title="Verdict and reason (ApprovalDecideForm.verdict, ApprovalDecideForm.comment, ApprovalDecideForm.reasonRequired)"> The verdict is set by the button you pressed and cannot be changed in the confirmation: approve, request changes or reject. The reason is up to 2000 characters, required for a rejection or a request for changes when the project's policy asks for one (the form knows and says so), optional otherwise; the author reads it beside your verdict. </Accordion> <Accordion title="Rule and quorum (ApprovalPolicyForm.rule, ApprovalPolicyForm.quorum)"> `any` carries a request on the first approval, `all` needs every approver named, `quorum` needs the number in **Quorum**, 1 to 50; a quorum larger than the approvers named means every approver. Default: any one approver. </Accordion> <Accordion title="Approver roles, named people and stakeholder seats (ApprovalPolicyForm.approver_roles, approver_user_ids, approver_stakeholder_ids)"> Up to 8 roles among owner, project manager, business analyst, developer, QA engineer, release manager and viewer; up to 50 people, each a member of the team; up to 50 stakeholders of the project. An enabled gate must name at least one source. Default for the requirement review: the project manager role. </Accordion> <Accordion title="Hours to decide, reminder after, approve by itself after (ApprovalPolicyForm.sla_hours, reminder_hours, auto_approve_after_hours)"> Each 1 to 720 hours or empty. Defaults for the requirement review: 72 hours to decide, a reminder after 24, never approving by itself. </Accordion> <Accordion title="Reason required on a refusal and gate on (ApprovalPolicyForm.requires_comment_on_reject, ApprovalPolicyForm.enabled)"> Both on by default. With the gate off a submission is approved the moment it is made, with no approvers and an approval marked automatic; the record still takes a new version. </Accordion> </Accordions> ## What happens next - Submitting emits `approval.requested` and, for a record with a review status, its family's `status_changed` event; the approvers are notified under the **Approvals** alert class. - Every verdict emits `approval.decided`; what it settles emits `approval.approved`, `approval.rejected` or `approval.changes_requested` beside it, and the author is notified. - Withdrawing emits `approval.withdrawn`; an overdue request emits `approval.escalated`; a saved policy emits `approval.policy_updated` with what changed. - An approval writes a version with the reason _approval_ on the record, visible in its history; the request keeps the id of the version that was read. - A client guest sees an epic, requirement, rule or use case from the moment it is approved, as before. - A baseline's approval freezes it and emits `baseline.frozen`; the client's sign-off emits `baseline.signed_off`; a change request's approval unlocks the baseline items it names and the request moves on by hand from there. See [baselines](/planning/baselines) and [change requests](/planning/change-requests). ## Heimdall on this page Heimdall reads approvals and never decides them. Ask _"What waits on me?"_ or _"Why is CRM-FR-12 still in review?"_ and it reads the inbox and the request with its verdicts. An agent working on a story can submit its draft for review once it is complete; the people decide. The [council](/copilot/council)'s requirement review is the reading to ask for before you approve a story, requirement or rule you are unsure of. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | -------------------------------------------------- | ------------------------------------------------------------------ | | Submit, resubmit or withdraw your own | The record's update permission, as staff | | Decide | Named on the request by the policy; `approval:decide` over the API | | Withdraw another person's request | `approval:withdraw` | | Read the inbox and a project's requests | `approval:view-any`, `approval:view` | | Change the policies | `approval:manage-policies` | | Submit a baseline or ask for the client's sign-off | `baseline:freeze`, as staff | | Submit a change request | `change-request:approve`, as staff | ## Frequently asked **Can I approve my own submission?** When the policy names you, yes; the engine does not refuse it. Name someone else on the policy where a second pair of eyes is the point. **The status menu lost In review and Approved. Where did they go?** To the bar. Submitting moves a record into review; the approvers' decisions move it out. While a request is open, the status menu is closed; withdraw the request first. **What does a client guest see?** Only the requests that name them through a stakeholder seat linked to their account, in the inbox and on the record once it is visible to them; they decide those and submit nothing. The sign-off of a scope baseline names every seat flagged as approving requirements, and a change request names the seat that asked for it. See [approving as a client](/guests/approving-requirements) and [signing off the scope](/guests/signing-off-scope). **Nobody was asked. Why?** The policy's roles matched no one on the project. On a members-only project only membership rows count; on a whole-team project the team role counts too. Add a person to the project with the role, or name them by name. ## Related - [Revisions and versions](/planning/revisions-and-versions) — the version an approval binds to, and the one it writes - [Project settings](/projects/project-settings#set-the-approval-policies) — where the policies live - [Stakeholders](/projects/stakeholders) — the seats a policy may name - [Notifications](/account/notifications) — where the approvals alert class is routed - [Status reference](/reference/status-reference#approval-status) — every approval status and verdict - [Approving as a client](/guests/approving-requirements) — the guest's side of the same request - [Baselines](/planning/baselines) — the two gates a scope baseline passes on this bar - [Change requests](/planning/change-requests) — the gate whose approval unlocks a frozen record --- # Attachments Source: https://alm-docs.labs.zuselab.dev/planning/attachments By the end of this page you will have pinned a wireframe on a story and a signed specification on a requirement, opened them from the flyout, known who sees them and how a file reaches a browser, and removed one; you will also know which kinds of file are accepted and how big one may be. An **attachment** is a file pinned on one artefact: a wireframe on a story, a signed specification on a requirement, a sample export on a data entity, a vendor quote on a decision. Files live in the **Files** panel at the foot of the artefact's flyout, between its links and its discussion, on every page that has one: epics, work items, requirements, business rules, use cases, RAID items, decisions, data entities and glossary terms. The record keeps the file's name, size and kind and who uploaded it; the bytes live on a private disk and leave it only through the application, to a person who may see the artefact, never through a URL anyone can guess. An attachment is not versioned: it is added or removed, and both are announced. ## Before you begin | You need | Where it comes from | | --------------------- | ---------------------------------------------------------------------------------------------------- | | An artefact | Anything from the [planning](/planning) pages or the [dictionary](/projects/glossary). | | `attachment:view-any` | To see the panel and open the files; every role has it, a client guest for the agreed record. | | `attachment:create` | To upload; the roles that write planning artefacts have it, a client guest does not. | | `attachment:delete` | To remove someone else's file; your own needs only `attachment:create`. | ## Open the files of an artefact [#open] 1. Open the artefact's page and open the artefact: its flyout opens from the row's menu, from **View**, or from a `?show=` link someone sent you. 2. Scroll to **Files**, under the artefact's links. Each row is one file, newest first: a picture shows as a small preview, any other file as a badge with its kind; then the name, which opens or saves the file, the size, who uploaded it and when (the full date on hover), and a remove button when you may remove it. ## Attach files [#attach] 1. Under **Add Files**, pick up to ten files at once. Images, PDFs, Office documents, text, CSV, JSON and ZIP files are accepted, up to 25 MB each unless the operator raised the ceiling. 2. Press **Attach**. The files appear at the top of the list, a toast counts them and `attachment.created` goes out for each. A pick with a file the panel refuses is dropped whole, with the reason under the field, so a refused file never lingers at the front of the next pick. ## Open or save a file [#download] The name of each row hands the file to the browser through the application, with the file's own name and type; a picture's preview opens it where it stands. Nothing is served from a public folder: a link to a file works only for a signed-in person who may see the artefact, which is why a file sent to a colleague is a link to the artefact, not to the file. ## Remove a file [#remove] Press the remove button at the end of a row, then **Remove** to confirm, or **Keep** to back out. The file is gone from the record and from the disk, and `attachment.deleted` goes out. You remove the files you uploaded; `attachment:delete` lets a moderator remove anyone's. ## All the fields <Accordions> <Accordion title="Add files"> Up to ten files at a time, each of an accepted kind and under the size ceiling. The name the file had on your machine is kept; its kind (image, document, spreadsheet, presentation, archive, text or other) is read from its type when it is stored. </Accordion> </Accordions> ## What is accepted | Kind | Extensions | Shown as | | ------------ | -------------------------------------------- | -------------------------------- | | Image | `jpg`, `jpeg`, `png`, `gif`, `webp` | A preview that opens the picture | | Document | `pdf`, `doc`, `docx`, `odt`, `rtf` | A badge | | Spreadsheet | `xls`, `xlsx`, `ods`, `csv` | A badge | | Presentation | `ppt`, `pptx`, `odp` | A badge | | Archive | `zip` | A badge | | Text | `txt`, `md`, `json`, `log` | A badge | Executables, scripts and SVG files are refused on purpose: a file that can run is not a file a client's record should carry. The [status reference](/reference/status-reference#attachment-kind) lists the kinds with their codes. ## What happens next - `attachment.created` and `attachment.deleted` go out with the artefact, the file's name, size and kind and who uploaded it, never the bytes; see the [`attachment.*` events](/webhooks/v1/events/attachment). - A file is not a [version](/planning/revisions-and-versions) of the artefact: attaching and removing change nothing on the record itself, and the activity log keeps the file's own entry. - Heimdall and an agent connected over MCP read the files' names and kinds with [`list_attachments`](/mcp/v1/tools/planning/list-attachments) and fetch the bytes over the REST API with a token that may read them. ## Managing - **A moderator** with `attachment:delete` removes anyone's file. - **A client guest** sees and opens the files on the agreed record, and uploads nothing. - **An artefact deleted** takes its files with it. - **A deleted account** leaves its files in place, with the uploader shown as someone. - **The ceiling and the kinds** are the operator's to raise or narrow; this page describes the defaults. ## Heimdall on this page Heimdall and an agent connected over MCP read what is pinned on an artefact so a specification or a wireframe named in a story is found rather than guessed; the agent fetches the bytes over REST. Neither uploads or removes a file: what a record carries is a person's call. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------- | --------------------------------------------------------------------------- | | See and open the files | `attachment:view-any` and `attachment:view`; a client guest on the agreed record | | Attach files | `attachment:create`; never a client guest | | Remove your own file | `attachment:create` | | Remove anyone's file | `attachment:delete` | ## Frequently asked <Accordions> <Accordion title="Why was my file refused?"> Either it is over the ceiling (25 MB unless the operator raised it) or it is a kind the panel does not accept; the message under the field says which. Compress a large picture, export a document as PDF, and zip anything else. </Accordion> <Accordion title="Can I send someone a link to the file?"> Send them a link to the artefact instead: the file is served only to a signed-in person who may see the artefact, so a link to the file alone opens nothing for anyone else. </Accordion> <Accordion title="Does uploading a new version replace the old one?"> No. Attach the new file and remove the old one if it no longer applies; the two events keep the record of what was there. Versions of the artefact's own fields live in its [history](/planning/revisions-and-versions), not in its files. </Accordion> </Accordions> ## Related - [Comments and mentions](/planning/comments-and-mentions) — the discussion under the files - [Traceability](/planning/traceability) — the links above them - [Design references](/projects/design-references) — links to Figma files, prototypes and contracts, as opposed to files pinned on a record - [Status reference](/reference/status-reference#attachment-kind) — every kind with its code - [`attachment.*` webhook events](/webhooks/v1/events/attachment) --- # Backlog Source: https://alm-docs.labs.zuselab.dev/planning/backlog By the end of this page you will have written down the work of a project as stories, tasks, sub-tasks, bugs, spikes and improvements, know every field each kind carries and what reads it downstream, and know how an item moves along its lifecycle, who holds it, how it is deleted and where every version of it is kept. A **work item** is one row of the tracker. All six kinds share the project's tracker sequence, so a reference such as `CRM-142` is always one thing whether it is a story, a bug or a spike, and each kind adds its own fields to the ones they share: a story's persona and its _as a / I want / so that_ sentence, a bug's severity, environment and reproduction, a spike's time box and outcome. A story also carries [acceptance criteria](/planning/acceptance-criteria), the sentences that say when it is done, and every save of an item is kept as a [version](/planning/revisions-and-versions) with the criteria inside it. ## Before you begin | You want to | You need | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Open the Backlog page | `work-item:view-any` on the project, which every default role carries. A client guest sees the agreed items only. | | Add an item | `work-item:create`. Project managers, analysts, developers and QA have it by default. | | Edit an item | `work-item:update`. | | Move an item along its lifecycle | `work-item:transition`. | | Hand an item to someone | `work-item:assign`. | | Delete an item | `work-item:delete`. | | Name a person on an item | The person to be a member of the team. | The page lives under a project: open the project and choose **Backlog** in the sidebar, badged with how many open items you may see. An epic the items hang under is written first, on the [Epics](/planning/epics) page. ## Read the backlog [step] The table lists one item per row: | Column | What it shows | | ------------- | --------------------------------------------------------------------------------------------------------------------------- | | **Reference** | The item's key, painted in the colour of its kind. | | **Item** | The title, with the epic it hangs under and the parent it sits under beneath it. Press the title to open the detail flyout. | | **Kind** | Story, task, sub-task, bug, spike or improvement, as a badge with the kind's icon. | | **Status** | Backlog, ready, in progress, in review, in QA, done or cancelled, as a badge. Hidden from a guest. | | **Priority** | Lowest to highest, as a badge with an arrow. | | **Assignee** | The person doing the work, or _Unassigned_. Hidden from a guest. | | **Points** | The story points, or _Not sized_. | | **Updated** | How long ago the record last changed. | | Row menu | **View Item**, **Edit Item**, **Move to**, **Assign to**, **Sprint**, **Activity**, **Delete Item**. | Sort by reference, title, kind, status, priority, points or updated by pressing a column heading; press it again to flip the direction. The list opens in the project's own order. Above the table, narrow the list with the search box (words in the reference or the title), the **kind**, **status** and **priority** lists, the **epic** list and the **assignee** list. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Add an item [step] 1. Press **New Item**. 2. Pick the **Kind** and the **Priority**. A story is the default; the fieldset the kind needs appears as soon as you pick it. 3. Type the **Title**. It is the only field required; the rest can follow, and Heimdall will draft stories from the epics and the brief once the brief import lands. 4. Write the **Description** and pick the **Epic** the item hangs under. A sub-task also needs a **Parent** (the story, task, bug or improvement it belongs to); a bug may name the story it was found in. 5. Fill the fieldset of the kind: for a story the **Persona** and the sentence (**As a**, **I want**, **So that**) with the **Business Value**; for a bug the **Severity**, whether it is **Reproducible**, the **Environment** it was seen in, the component it was **Found In**, the **Steps to Reproduce** and the **Expected** and **Actual Result**; for a spike the **Time Box** and, when it closes, the **Outcome**. 6. Under _Who_, pick the **Assignee**, the **Reporter** (you, unless it came from someone else), the **Reviewer** and the **QA Owner**; under _Size and Dates_ the **Story Points**, the **Due** date, the **Estimate** and what is **Remaining**. 7. Pick the **Labels**, tick the **Components Touched** and write the [acceptance criteria](/planning/acceptance-criteria). 8. Press **Create Item**. The item takes the next number of the project (the first item of `CRM` after its epics is the next in the same series), starts in the backlog, names you as its reporter unless you said otherwise, appears in the table at once, writes its first version with its criteria inside it, and `work_item.created` goes out to every endpoint subscribed to it. <Callout type="info" title="One sequence for the tracker"> Epics, stories, tasks, sub-tasks, bugs, spikes and improvements share one number series per project, so `CRM-142` is always one thing and a reference in a commit or a conversation never needs a kind. A number is never reused, even after a delete. </Callout> ## Open an item [step] Press an item's title, or **View Item** in its row menu, to open the detail flyout: the reference, kind, status, resolution, priority and severity at the top, the title and the story sentence, then the description, the fields of the kind, the acceptance criteria, the epic, the parent, the people, the sizes, the dates, the components touched and the **Versions** section with every save of the item. From the flyout: - **Edit** opens the editor on this item. - **Move to** changes the status in one step, offering only the moves the lifecycle allows, with the reason to pick when the move is into cancelled. - **Assign to** hands the item to you, to a member of the team or to nobody. - **Sprint** plans the item into a planned or running [sprint](/building/sprints) of the project, or sends it back to the backlog; the editor's **Sprint** field does the same, and the backlog's sprint filter shows one sprint's items or the items in no sprint. - **Restore this version** under any earlier version puts it back, criteria included; see [revisions and versions](/planning/revisions-and-versions). - **Activity** opens the item's history: every change, who made it and where it came from. - **Delete** opens the confirmation. A link such as `/projects/<id>/backlog?show=<item id>` opens the page with that item's flyout already open; Heimdall's **View** buttons use it. ## Edit an item [step] 1. Press **Edit Item** in the row menu, or **Edit** in the flyout. 2. Change what you need, the criteria rows included. Every person named must be a member of the team; the epic, the persona, the environment and every component must be the project's; a sub-task keeps a parent and a parent is a story, task, bug or improvement of the same project. Changing the kind clears the fields the new kind does not carry. 3. Press **Save Changes**. The save writes a new version of the item, criteria included, and `work_item.updated` goes out with the columns that changed, each with its old and new value. An edit that changes nothing writes nothing and announces nothing. The status does not change from the editor; it moves from the list or the flyout. ## Move an item along its lifecycle [step] **Move to** in the row menu or the flyout offers the moves the lifecycle allows from where the item stands: | From | To | | --------------- | ---------------------------------------- | | **Backlog** | Ready, in progress, cancelled | | **Ready** | Backlog, in progress, cancelled | | **In progress** | Ready, in review, in QA, done, cancelled | | **In review** | In progress, in QA, done, cancelled | | **In QA** | In progress, done, cancelled | | **Done** | In progress | | **Cancelled** | Backlog | A move into **Cancelled** asks why: won't do, duplicate, cannot reproduce or deferred; a move into **Done** resolves the item as done. The first move into progress stamps when work started, a move into done stamps when it was finished, and a move back into an open status clears the resolution and the finish. Every move writes a version with the reason _moved_ and `work_item.transitioned` goes out with the status before and after and the resolution. A column the project caps takes no more once it is full. The **work-in-progress limits** under [project settings](/projects/project-settings) name the most items an open status may hold across the whole project; a move into a status at its limit is refused here, on the [board](/building/board), from an agent and over the API alike, and the message names the column, what it holds and the limit. A story's approval is separate from its board status. **Submit for review** on the [approval bar](/planning/approvals) in its flyout asks the people the project's policy names; the story keeps its status throughout, and the approval stamps it approved at a version, from which moment a client guest sees it. Only an open story is submitted; a task, a bug, a spike or a done story is not. <Callout type="info" title="The same move, three ways"> Items move from this page, from the flyout and from the [board](/building/board), where a card dragged into another column runs the same move and a card dragged up or down changes the item's place in the project's order, the one this page's default sort reads. The Git integration will move a story into review when its pull request opens and into QA when it merges, and the Definition of Done will hold a move into done until its checklist is ticked, as those modules land. </Callout> ## Hand an item over [step] **Assign to** in the row menu or the flyout lists **Me**, every member of the team and **Nobody**. Pick one and the item is theirs: the table and the flyout show the name, their [My Work](/mcp/v1/tools/build/list-my-work) reads it, and `work_item.assigned` goes out with the person before and after. Handing an item to the person who already holds it changes nothing and announces nothing. A person who is not a member of the team is refused. ## Delete an item [step] **Delete Item** in the row menu or **Delete** in the flyout opens the confirmation. Deleting removes the item with its criteria, every version it kept and its activity history; the items under it lose their parent and stay in the backlog, and its number is never handed out again. `work_item.deleted` goes out. Prefer **Cancelled** with a reason for work the team dropped: it keeps the record and the reasoning. ## All the fields <Accordions> <Accordion title="Kind"> Required, _story_ by default. Story, task, sub-task, bug, spike or improvement; the [status reference](/reference/status-reference#work-item-kind) says what each is for. The kind decides which fieldset the editor shows and which fields the record keeps; changing it clears the fields the new kind does not carry. A sub-task must hang under a parent; only a sub-task or a bug may. </Accordion> <Accordion title="Title"> Required, 3 to 200 characters. What everyone calls the item, next to its reference, in the table, the board, the commit messages and Heimdall's answers. A verb and an object read best. </Accordion> <Accordion title="Description"> Optional rich text. What the item is and why, in a paragraph or two; the person doing the work and the agent drafting tests read it first. Filtered to a safe subset when shown. </Accordion> <Accordion title="Epic"> Optional. The body of work the item belongs to, one of the project's epics; the roadmap and the release notes group by it, and the table shows its reference under the title. </Accordion> <Accordion title="Parent"> Required for a sub-task, optional for a bug, absent for the other kinds. The story, task, bug or improvement the item sits under, in the same project; an item cannot be its own parent. The table shows the parent's reference under the title. Deleting the parent frees the item. </Accordion> <Accordion title="Priority"> Required, _medium_ by default. Lowest, low, medium, high or highest; the [status reference](/reference/status-reference#work-priority) says what each means. The backlog sorts by it and the sprint planner reads it. </Accordion> <Accordion title="Persona, As a, I want, So that and Business value"> The story fields, kept for a story only. The persona is one of the project's [personas](/projects/personas) and names the person in the sentence; the three parts are the sentence in their words (what they want to do, not how the system does it, and why it matters); the business value is a score in points on the project's scale, so the backlog can be ordered by value against size. The flyout reads the sentence back under the title. </Accordion> <Accordion title="Severity, Reproducible, Environment, Found in, Steps to reproduce, Expected and Actual result"> The bug fields, kept for a bug only. Severity is blocker, critical, major, minor or trivial (a blocker or a critical bug holds a release); reproducible is always, sometimes, once or not reproduced; the environment is one of the project's [environments](/projects/environments) and _found in_ one of its [components](/projects/components). The three rich-text fields are what to do to make it happen, what should have happened and what happened instead, with the error text when there is one. </Accordion> <Accordion title="Time box and Outcome"> The spike fields, kept for a spike only. The time box is how many minutes the investigation may take before it reports back, whatever it found; the outcome is written when the spike closes, and the decision or the stories that follow point back at it. </Accordion> <Accordion title="Assignee, Reporter, Reviewer and QA owner"> Optional, each a member of the team. Who is doing the work, who raised it (you, unless you say otherwise), who reviews the code before it moves to QA and who verifies it against the criteria. Their names are shown in the table and the flyout; a client guest sees none of them. </Accordion> <Accordion title="Story points, Estimate, Remaining and Due"> Optional. The estimate on the project's scale (up to ten characters, such as `5` or `M`), the estimate in minutes for projects that size in hours, what is left in minutes (the burndown reads it), and the date the item is due if one was agreed (the dashboard flags it once passed). </Accordion> <Accordion title="Components touched"> Optional, up to twenty of the project's components. The parts of the product the item changes; the release notes and the test plans group by them. Shown only when the project has components. </Accordion> <Accordion title="Labels"> Optional, up to twenty of the project's own words, picked from a searchable list; **Manage Labels** beside the heading adds a word without leaving the editor. Shown as chips on the row and in the flyout; the backlog filters by one. The [labels](/planning/labels) page describes the vocabulary. They are not part of the item's versions. </Accordion> <Accordion title="Acceptance criteria"> The rows that say when the item is done; the [acceptance criteria](/planning/acceptance-criteria) page describes them. They travel with every version of the item. </Accordion> <Accordion title="Status, resolution, reference, number and the stamps"> Read-only in the editor. The status and the resolution move from the list or the flyout; the reference and the number are assigned when the item is created; started and completed are stamped by the moves, approved by the [approval](/planning/approvals) that carries; created and updated by every save. The flyout also reads the names behind the ids: the epic's and the parent's reference, the people's names, the persona, the environment, the component and the components touched. </Accordion> </Accordions> ## What happens next - Every save, move, hand-over and restore writes a [version](/planning/revisions-and-versions), criteria included, and an entry in the project's [activity history](/workspace/activity-log). - `work_item.created`, `work_item.updated`, `work_item.transitioned`, `work_item.assigned`, `work_item.ranked`, `work_item.criteria_changed`, `work_item.restored` and `work_item.deleted` go out to the webhook endpoints subscribed to the `work_item.*` family; `work_item.ranked` carries the position an item took when it was dragged to a new place on the [board](/building/board) or ranked over the API. - The board, the sprints, the time logs, the Git links, the test cases drafted from the criteria and the releases composed from done items will all read the backlog as those modules land. ## Heimdall on this page **Quality** sits in the flyout above the links: **Review now** asks Heimdall to read the item against INVEST, its criteria, the project's business rules and its siblings, score it out of 100 and list what to fix, quoting the words and suggesting the rewrite; the row then carries the score beside its reference. See [Quality review](/planning/quality-review). Heimdall reads the backlog and every item's versions through the same tools the MCP server offers, drafts stories into it as drafts a person reviews, moves and assigns items when you ask, and carries the open project as its context. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “Which stories in the backlog have no acceptance criteria yet?” · “Draft a story under the loyalty tiers epic for a member who wants to see how far they are from the next tier.” · “Move CRM-142 to in review and assign it to Nimal.” A story Heimdall drafts lands in the backlog as a draft with its sentence and its criteria; the brief import will draft whole epics' worth of them into a review queue once it lands. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------- | --------------------------------------------------------------------------------- | | See the backlog | `work-item:view-any`, `work-item:view`; a client guest sees the agreed items only | | Add an item | `work-item:create` | | Edit an item or its criteria | `work-item:update` | | Move an item | `work-item:transition` | | Hand an item over | `work-item:assign` | | Delete an item | `work-item:delete` | | Read or restore its versions | `work-item:view` to read, `work-item:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="Why does a guest see none of my items?"> A story is the team's working note until it is approved. A client guest sees an item once an [approval](/planning/approvals) stamps it; until then the backlog is the team's. </Accordion> <Accordion title="Can I turn a task into a story, or a story into a bug?"> Yes: change the **Kind** in the editor. The fields the new kind does not carry are cleared on save and kept in the version before, so nothing is lost; the reference stays the same. </Accordion> <Accordion title="Why can I not move an item straight from the backlog to done?"> The lifecycle only allows the moves in the table above, so a status always says something true about the work. Move it into progress first; the stamps follow the moves. </Accordion> <Accordion title="What happens to the sub-tasks of a deleted story?"> They lose their parent and stay in the backlog with their own references. Cancel the story instead when the work itself was dropped; cancelling keeps the record and leaves the sub-tasks attached. </Accordion> </Accordions> ## Related - [Quality review](/planning/quality-review) — the Quality panel in the flyout: Heimdall's score and findings, and the badge on the row - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [Labels](/planning/labels) — the project's own words on its items, and the filter by one - [Acceptance criteria](/planning/acceptance-criteria) — the rows that say when a story is done - [Epics](/planning/epics) — the bodies of work the items hang under - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [Status reference](/reference/status-reference#work-item-kind) — every kind, status, resolution, priority, severity and reproducibility - [`work_item.*` webhook events](/webhooks/v1/events/work-item) --- # Baselines Source: https://alm-docs.labs.zuselab.dev/planning/baselines By the end of this page you will know what a baseline is, how to compose one from the records the team approved, how the project manager's approval freezes it and what that freeze does to every record it holds, how the client signs a scope baseline as a second gate, how a frozen baseline is read and superseded, and the one way a frozen record changes afterwards: an approved [change request](/planning/change-requests). A baseline is a named set of approved records frozen together: the scope the client signs before the build starts, what a release ships, what a sprint commits to, what a milestone promised. Until it is frozen it is a draft list anyone with the permission composes and recomposes. Once the project manager approves it, every record it holds is pinned to the version that was approved and locked: the record's editor, its status menu and its restore button all refuse until a change request that names the baseline and the record is approved. A scope baseline then goes to the client's stakeholders for their signature, and the record the project is held to is the one they signed. Three Norns hold the frozen record; nothing moves without their say. ## Before you begin | To | You need | | --------------------------------------------- | ---------------------------------------------------------------------------------------------- | | See the baselines | `baseline:view-any` and `baseline:view`; a client guest sees the frozen ones | | Compose a draft and change it | `baseline:create`, `baseline:update` | | Submit a draft for approval, ask for the sign | `baseline:freeze` | | Approve a baseline | To be named by the project's **Baseline approval** policy; the project manager role by default | | Sign a scope baseline | A stakeholder seat flagged **Approves requirements** with your guest account linked to it | | Supersede a frozen baseline | `baseline:update` | | Delete a draft | `baseline:delete` | Only approved records join a baseline: an epic, a story, a requirement, a business rule or a use case that carries a version. Approve the records first through their own [approval](/planning/approvals); the picker offers nothing else. ## Open the page [step] **Baselines** in the project's sidebar opens the page, under its **Baselines** tab; the **Change Requests** tab beside it is the [other half](/planning/change-requests) of the same family. The badge on the sidebar entry counts the change requests still open, not the baselines. The list shows each baseline's name and the first line of its description, its kind, its status, how many records it holds, when it was frozen, when the client signed it (a dash on a kind the client does not sign) and when it last changed. Sort by name, kind, status, the frozen date, the signed date or the last change; filter by kind or status, or search by name; **Clear Filters** drops every filter. A client guest opens the same page and sees the frozen baselines only: approved, awaiting the client's signature, signed, or superseded. A draft or a baseline in review is not theirs to see. ## Compose a draft [step] Press **New Baseline**. The editor asks for a **Name**, unique within the project (_v1.0 scope_, _Release 1.1_, _Sprint 3_), a **Kind** and an optional **Description** of what the baseline marks and why it was cut; the client reads the description beside the records. Under **Records**, the picker lists every approved record the project holds, each with its family, its key and its title (_Requirement · CRM-FR-12 Password reset by email (v2)_); pick the ones the baseline freezes. Press **Create Baseline**. The draft is written with its records in the order you picked them, every record noted at the version it is approved at today; nothing is frozen yet. A draft changes freely: **Edit Baseline** in the row menu or **Edit** in the flyout reopens the editor with the name, the kind, the words and the records, and **Save Changes** writes them, dropping the records you removed and adding the ones you picked. Every save is a [version](/planning/revisions-and-versions) of the baseline. <Callout type="info" title="Four kinds, one engine"> **Scope**, **Release**, **Sprint** and **Milestone** differ in one way: a scope baseline is signed by the client after the team approved it. The other three are frozen by the team's approval alone. Pick the kind before you submit; it is part of the record the approvers read. </Callout> ## Submit it for the team's approval [step] Open the baseline's flyout. The **Approval** bar offers **Submit for review** once the draft holds at least one record; an empty draft cannot be submitted. Write a few lines for the approvers under **Summary** and confirm. The baseline moves to **In review**, the approvers of the project's **Baseline approval** policy are told (the project manager role by default, under the **Any one approver** rule), and the draft stops changing until they answer. What the approval does when it carries: - The baseline becomes **Approved** at version 1, stamped with when it was frozen and by whom. - Every record it holds is pinned to the revision that was approved and marked frozen. - A requirement or a business rule it holds moves from **Approved** to **Baselined**, with a version written under the reason _baselined_; an epic, a story or a use case keeps its status. - Each record is now locked: editing it, moving it, deleting it or restoring an earlier version of it is refused with the baseline named, until a change request that names it is approved. - `baseline.frozen` goes out beside `baseline.status_changed`, with the number of records held. A rejection returns the baseline to **Draft** with nothing frozen; the approver's reason is on the request. A request for changes holds it until you edit the draft and resubmit. ## Have the client sign the scope [step] A frozen scope baseline shows a callout under its approval bar: _Frozen by the team. Submit it again from the approval bar to ask the client's stakeholders to sign the scope._ Press **Submit for review** again. This second submission opens a **Client sign-off**, not a baseline approval: the approvers are every stakeholder of the project flagged **Approves requirements** whose guest account is linked to their seat, under the **Every approver** rule, so the scope stands signed only when each of them agrees. The baseline reads **Awaiting sign-off** meanwhile, its records as locked as before. When every stakeholder approves, the baseline becomes **Signed off** at version 2, stamped with the date of the signature; the callout turns green and names the date, and `baseline.signed_off` goes out. A stakeholder who rejects returns it to **Approved**, still frozen, with their reason on the request; the team reads it, raises what it must as a change request, and asks again. A release, sprint or milestone baseline never takes this step: its approval is its last gate. <Callout type="warn" title="Nobody to sign"> The sign-off resolves its approvers from the stakeholder seats, not from the policy's roles. A project whose stakeholders have no linked guest account, or none flagged as approving requirements, cannot submit the sign-off; the message says so. Flag the seats and invite the people under [stakeholders](/projects/stakeholders) first. </Callout> ## Read a frozen baseline [step] Open a row for the flyout: the kind, the status and the version as badges, the name, the approval bar with the request that froze or signed it, the client callout on a scope baseline, the description, and the **Records** table: each record's key, title, the version held and whether it is **Held**, **Unlocked** by a change request, or still a **Draft** row of a baseline not yet frozen. Below sit the clocks (frozen and by whom, signed off, created, updated), the **Versions** of the baseline itself, its links, files and comments, and **Activity**. A client guest reads the same flyout without the versions, the links and the activity. Open a record held by the baseline and its own flyout says so too: the editor is refused with the baseline's name, and the status menu offers nothing the lock forbids. ## Supersede a baseline [step] A frozen baseline is never edited and never deleted; it is the record. When a newer baseline replaces it, choose **Move to → Superseded** in the row menu or the flyout. A superseded baseline stays readable with every stamp it carried, but it no longer holds its records: a record only it held is free to change again, and a requirement or a rule that still reads **Baselined** is moved on by hand or by the next baseline that holds it. Only an approved or a signed baseline moves to superseded; the moves through **In review**, **Approved**, **Awaiting sign-off** and **Signed off** are the approval engine's and the menu does not offer them. ## Delete a draft [step] **Delete Baseline** in the row menu or **Delete** in the flyout opens the confirmation. A draft is removed with its record rows and its versions; its name is free again. A baseline past its draft cannot be deleted; supersede it instead. ## All the fields <Accordions> <Accordion title="Name (BaselineForm.name)"> 3 to 120 characters, unique within the project regardless of case; the client signs the scope by this name and the approval request carries it as its reference. Changed only while the baseline is a draft. </Accordion> <Accordion title="Kind (BaselineForm.kind)"> `scope`, `release`, `sprint` or `milestone`; default scope. A scope baseline is signed by the client after the team's approval; the other kinds are frozen by the team's approval alone. Changed only while a draft. </Accordion> <Accordion title="Description (BaselineForm.description)"> Rich text up to 20,000 characters, optional: what the baseline marks and why it was cut. Shown in the flyout and, in its first line, under the name in the list. The client reads it beside the records. </Accordion> <Accordion title="Records (BaselineForm.items, BaselineForm.item_rows)"> The picker holds up to 500 approved records, each named by its family and id; the picker offers only records that carry a version (an epic, a story, a requirement, a business rule or a use case the project approved), and the save refuses any other. The flyout shows the rows as they were written: the key and title as they read when added, the version held, and whether the record is held, unlocked by a change request, or a draft row. </Accordion> <Accordion title="Status, version and the clocks (BaselineForm.status, version, frozen_at, frozen_by_name, signed_off_at)"> Read-only. The status moves through the approval engine (draft, in review, approved, awaiting sign-off, signed off) and by hand only to superseded. The version is 0 while never approved, 1 once the team approved it and 2 once the client signed. Frozen at and by whom are stamped by the team's approval; signed off at by the client's. </Accordion> <Accordion title="Id, created and updated (BaselineForm.id, BaselineForm.created_at, BaselineForm.updated_at)"> Read-only: the baseline's id, when it was added and when it last changed, shown in your own timezone. </Accordion> </Accordions> ## What happens next - Composing a baseline emits `baseline.created`, and every change to its words or its records `baseline.updated` with what changed (the records as the list of keys before and after). - The approval engine emits `approval.requested`, the verdicts, and `baseline.status_changed` on every move; the freeze adds `baseline.frozen` and the signature `baseline.signed_off`, each with the approval that carried it and the version reached. - Superseding emits `baseline.status_changed`; deleting a draft emits `baseline.deleted`; putting an earlier version of a draft back emits `baseline.restored`. - A frozen record refused over the API answers `BASELINE_LOCKED` with the baseline's id in the error's context; the application shows the same refusal as a message naming the baseline. - The [project overview](/projects/overview) and the sidebar count the open change requests; the releases will cut their scope from a release baseline, and the sprints from a sprint baseline, as those modules land. ## Heimdall on this page Heimdall reads baselines and composes them when asked: _"Freeze the approved requirements and the loyalty epic as the v1.0 scope"_ composes the draft from the records it finds approved and tells you to submit it; _"What does the signed scope hold?"_ lists the records and the versions held; _"Which requirements changed since the client signed?"_ compares the held versions with the records as they stand. Heimdall never approves, signs or supersedes a baseline; those stay with the people named. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | -------------------------------------------- | ----------------------------------------------------------------------------------- | | See the baselines and open one | `baseline:view-any`, `baseline:view`; a client guest sees the frozen baselines only | | Compose a draft | `baseline:create` | | Change a draft's words or records, supersede | `baseline:update` | | Submit for approval, ask the client to sign | `baseline:freeze`, as staff | | Approve | Named by the **Baseline approval** policy; `approval:decide` over the API | | Sign a scope baseline | A stakeholder seat flagged **Approves requirements**, linked to the guest's account | | Delete a draft | `baseline:delete` | | Read and restore the baseline's own versions | `baseline:view`, `baseline:update`; staff only | ## Frequently asked **Can I add a record to a frozen baseline?** No. A frozen baseline is the record the client agreed to. Compose the next baseline with the new record and supersede the old one, or raise a change request if what changes is a record the baseline already holds. **A requirement reads Baselined but no baseline holds it. Why?** Its status was moved by hand, which the status menu still allows for an approved requirement or rule. The lock comes from a frozen baseline, not from the badge; a requirement no frozen baseline holds is free to change whatever its badge says. The freeze and the unlock move the badge themselves. **The client signed, and now the scope must change.** That is what a [change request](/planning/change-requests) is for. It names the signed baseline and the records it touches; when the approvers and the stakeholder who asked agree, those records, and only those, are unlocked. The signed baseline keeps its signature. **Why can a guest see a baseline awaiting their signature but not one in review?** A baseline in review is the team's draft under the team's gate. From the moment the team's approval freezes it, it is the client's record too, so every frozen status is visible to them. ## Related - [Change requests](/planning/change-requests) — the one way a frozen record changes - [Approvals](/planning/approvals) — the engine behind both gates, the policies and the inbox - [Signing off the scope](/guests/signing-off-scope) — the client's side of the signature - [Revisions and versions](/planning/revisions-and-versions) — the versions a freeze pins - [Status reference](/reference/status-reference#baseline-status) — every baseline kind and status - [Stakeholders](/projects/stakeholders) — the seats that sign --- # Brief import Source: https://alm-docs.labs.zuselab.dev/planning/brief-import By the end of this page you will have handed a client's brief to Heimdall, watched it come back as a queue of epics with their stories, requirements, business rules, use cases, glossary terms, personas, stakeholders, risks, assumptions and decisions, and accepted, edited, merged or rejected each one; you will also know what Heimdall reads before it drafts, how a story lands under its epic, what an accepted record remembers about its origin, what the import costs and what it announces. A **brief import** is the fastest way from a client's words to a planning record. You give the minimum, a paragraph or the whole brief, and Heimdall does the expanding: it reads the brief against what the project already holds, drafts one **suggestion** per record a good analyst would write, quotes the passage each one came from, says how sure it is and why, names the records the brief repeats, and adds **improvements**, the gaps a project of this kind leaves open. Nothing is written to the project by the reading. You decide each suggestion on the **review queue**, and only an accepted suggestion becomes a record, through the same door the record's own page uses. The page is **Brief Import** in the Plan group of the project sidebar. A client guest never sees it. ## Before you begin | To | You need | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | | Open the page and read a batch | `import:view-any` and `import:view` on the project; a staff account, never a client guest. | | Hand a brief to Heimdall | `import:create`, the project's **AI assists** switch on under Settings → AI, and room in the team's monthly AI budget. | | Accept, edit, merge or reject | `import:update`, plus the create permission of the record a suggestion becomes (an epic needs `epic:create`). | | Discard a batch | `import:delete`. | A brief is plain text or Markdown, at least a paragraph and at most 60,000 characters, pasted or uploaded as a `.txt` or `.md` file of up to 2 MB. PDF and Word files join with the knowledge module; until then, paste their text. ## Importing a brief <Steps> <Step> ### Press New Import and give Heimdall the words **Title** names the batch on the list. **Source** is where the words come from: paste them into the **Brief** box, or pick **Uploaded file** and choose a text or Markdown file. Keep the client's own words: Heimdall quotes the passages it draws on, and a reviewer reads the quote beside each suggestion. </Step> <Step> ### Press Import The batch is queued and opens at once with **Reading** on it. Heimdall reads the brief on the queue as you, so every record it checks for duplicates is one you may see; the page refreshes on its own and the sidebar badge counts the batch until it is reviewed. A brief takes a minute or two. You can leave the page and come back: the batch waits on the list. </Step> <Step> ### Read the queue A ready batch shows the brief on the left and the suggestions on the right, grouped by kind in the order an analyst works: epics with their stories beneath them, then requirements, business rules, use cases, glossary terms, personas, stakeholders, risks, assumptions, decisions and improvements. Each card carries the title, the main fields, the rows (criteria, examples, steps or aliases), how sure Heimdall was, and one line on why it proposed the suggestion. **Show passage** lights up the sentence of the brief the suggestion was drawn from. </Step> <Step> ### Decide each suggestion **Accept** writes the record as proposed. **Edit and accept** opens an editor built from the kind's own fields, so you change the title, the statement, the criteria or the category before the record is written. **Merge into…** points the suggestion at a record the project already holds, when Heimdall flagged one with **Looks like** or you know of one, and writes nothing. **Reject** drops it. An improvement, which proposes no record, is **kept** as a note or **dismissed**. **Accept All Pending** takes a whole kind at once, epics first so the stories can hang under them. </Step> <Step> ### Watch the records land An accepted suggestion shows **In the project** with a link to the record on its own page. The counts at the top of the queue follow, and a batch whose every suggestion is decided is marked **Reviewed**. The records are drafts like any other: give them their owners, their estimates and their approvals as you would anything written by hand. </Step> </Steps> <Callout type="info"> A story accepted after its epic lands under the epic Heimdall proposed it for, because the epic's suggestion remembers the record it wrote. Accept the epics first, or take the Epics group with **Accept All Pending** before the Stories group; a story accepted before its epic lands in the backlog without one, and you set the epic on the story afterwards. </Callout> ## All the fields of a suggestion <Accordions> <Accordion title="Epic"> Title, summary, goal and business value. Heimdall takes the goal as one sentence on what done looks like and the business value as what the client gets. </Accordion> <Accordion title="Story"> Title, the three parts of the story sentence (as a, I want, so that), the description and the acceptance criteria, one per line as "Given …, when …, then …". A line that does not read as Given, When, Then is kept as a plain criterion. The story hangs under the epic suggestion it was proposed for. </Accordion> <Accordion title="Functional requirement"> Title, the statement ("The system shall …") and the rationale. </Accordion> <Accordion title="Non-functional requirement"> Title, category (performance, security, accessibility and the rest), statement, rationale and the measure: a number, a unit and a condition, which the quality review will later hold the requirement to. </Accordion> <Accordion title="Business rule"> Name, kind (constraint, derivation, computation, inference, action enabler or policy), the rule in one sentence, the condition ("When …"), the action ("Then …"), the exceptions and the worked examples, one per line as "input → expected". </Accordion> <Accordion title="Use case"> Title, goal, trigger, preconditions, postconditions and the main flow, one step per line as "actor action → system response"; the alternate and exception flows are added on the Use Cases page afterwards. </Accordion> <Accordion title="Glossary term"> The term, its definition and its aliases, one per line. </Accordion> <Accordion title="Persona"> Name, tagline, role, goals and frustrations. The picture, the devices and the proficiency are added on the People tab. </Accordion> <Accordion title="Stakeholder"> Name, title, organisation, role on the project and side (client, the house or a third party). The approver flags, the RACI letter and the account link are set on the People tab; a persona and a stakeholder cannot be merged from the queue, so reject the suggestion and add what is new by hand when the record exists. </Accordion> <Accordion title="Risk and assumption"> Title, description, area and, for a risk, probability and impact from 1 to 5 and the mitigation. Both land on the RAID log as open items. </Accordion> <Accordion title="Decision"> Title, kind (architecture, product, process, vendor or security), context, what was decided and the consequences. The decision lands as proposed; the people who decided are added on the RAID log's Decisions tab. </Accordion> <Accordion title="Improvement"> A title and a summary: a gap or a question for you, such as a missing non-functional target, an undefined term or a persona without goals. It writes no record; keep it as a note on the batch or dismiss it. </Accordion> </Accordions> ## What Heimdall reads and how it judges Before it drafts, Heimdall is given the project's name and description, every record the project already holds by reference and title (epics, stories, requirements, rules, use cases, RAID items and decisions), the names of its terms, personas and stakeholders, the house stack, and the brief itself. It is told to draft from the brief and not from imagination; where it adds what any product of this kind needs, it says so in the rationale and lowers its confidence. It proposes at most sixty suggestions, the records before the improvements, and names a record of the project as a duplicate when a suggestion repeats it, so you merge instead of doubling. A brief that names another stack gets a decision suggestion recording the choice. **Confidence** is how directly the brief supports a suggestion, shown as a percentage: green from 80, amber from 50, grey below. **Why** is Heimdall's one-line reason. Neither decides anything; they tell you where to look twice. ## Where a batch stands | Status | Meaning | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Queued** | Handed to Heimdall; the reading has not started. | | **Reading** | Heimdall is reading the brief; the page refreshes on its own. | | **Ready to review** | The suggestions wait for you. | | **Reviewed** | Every suggestion was decided. | | **Discarded** | A person dropped the batch; its suggestions stay readable but cannot be decided. | | **Failed** | Heimdall could not read the brief: the model did not answer, the budget had no room, or the person who started it is gone. **Start Again** hands the same brief over as a new batch. | A suggestion is **Pending** until you decide it, then **Accepted**, **Accepted with edits**, **Merged**, **Rejected** or, for a note, **Dismissed**. A decision is never undone: the record it wrote has its own lifecycle, and a merged or rejected suggestion stays on the batch for the record. ## Convening the council on a batch Once a batch is **Ready to review** or **Reviewed**, people who may use Heimdall see **Convene the Council** in its header. Every voice the project seats on the intake scaffold reads the brief and what Heimdall drafted from it, with the decision so far on each row, and says what the drafts get wrong, what a project of this kind needs that neither the brief nor the drafts say, and what the client will be held to; a voice that can draft the missing record does, so accepting its finding writes the record. The Heimdall panel opens on its **Council** tab to show the sitting as it fills in, and the [council](/copilot/council) page has the voices, the verdict and the cost. ## What an accepted record remembers The record is written through its own create action, so it takes the next number of the project, passes the record's own rules, announces its own `created` event and starts its history with a first version. That version is written with the reason **Imported**, by **Heimdall** and accepted by you, and the activity log says the same, so anyone reading the record later knows where it came from. The suggestion keeps a link to the record, and the record's page shows the history like any other. ## What happens next - `import.started` goes out when the brief is handed over; `import.ready` or `import.failed` when the queue is done; `import.discarded` when a batch is dropped. - `import.suggestion_accepted`, `import.suggestion_rejected` and `import.suggestion_merged` follow each decision, beside the `created` event of the record an accepted suggestion wrote. - The reading is filed in the team's AI ledger under **Brief imports** and counts against the month's budget on [Settings → AI](/workspace/ai-settings); a batch at the cap fails before the model is asked, and **Start Again** works once the budget has room. - The sidebar badge on **Brief Import** counts the batches still reading or waiting for review. - The brief stays with the batch, so a second import of the same words can be read beside the first and what was already accepted or rejected can be told apart. ## From the API and MCP <Cards> </Cards> There is no MCP tool for the review queue on purpose: a brief import spends the budget on a whole brief and fills a queue for people, and an agent never accepts a record on Heimdall's say-so. An agent reads the records people accepted through the planning tools like any other. ## Permissions | Action | Permission | | ----------------------------- | ------------------------------------------------------------------ | | See the page and open a batch | `import:view-any`, `import:view`; never a client guest | | Hand a brief to Heimdall | `import:create`, with the project's AI assists on | | Accept, edit, merge or reject | `import:update`, plus the record's own create permission on accept | | Discard a batch | `import:delete` | | Read and decide over REST | The same abilities on the token | ## Frequently asked **Can I import a PDF or a Word document?** Not yet: paste its text, or save it as `.txt` or `.md` and upload that. The knowledge module adds the two formats. **What if the brief is longer than 60,000 characters?** Split it by topic and import each part; the second import sees the records the first one wrote and names them as duplicates. **Why does a story show no epic?** It was accepted before its epic. Set the epic on the story's editor on the Backlog, or accept the epics first next time. **Can I undo an accept?** No; delete the record on its own page if it should not exist. The suggestion keeps the link to the record it wrote. **Why did Heimdall propose a decision about the stack?** The brief named a technology outside the house stack. Heimdall assumes the house stack unless the brief rules it out, and records the choice as a decision for you to confirm or reject. **Does the import run as me?** Yes. The queue reads the brief with your permissions and meters the call to you, so the records Heimdall checks for duplicates are the ones you may see. ## Related - [Heimdall and the brief import](/copilot/brief-import) — how the reader thinks and what it costs - [Heimdall research](/copilot/research) — the other way in: Heimdall reads the web for what the domain demands and leaves research and improvement notes on the project overview - [Epics](/planning/epics), [Backlog](/planning/backlog), [Functional requirements](/planning/functional-requirements), [Non-functional requirements](/planning/non-functional-requirements), [Business rules](/planning/business-rules), [Use cases](/planning/use-cases), [RAID log](/planning/raid-log), [Decisions](/planning/decisions) — where the records land - [Glossary](/projects/glossary), [Personas](/projects/personas), [Stakeholders](/projects/stakeholders) — the words and the people - [Quality review](/planning/quality-review) — read what Heimdall drafted with the same eyes a developer will - [Revisions and versions](/planning/revisions-and-versions) — the first version an accepted record starts with - [Status reference](/reference/status-reference#import-status) — batch and suggestion statuses, and the kinds --- # Business Rules Source: https://alm-docs.labs.zuselab.dev/planning/business-rules By the end of this page you will have written down the decisions of the business a project's system must honour as rules a developer can code and a tester can check, know every field a rule carries and what reads it downstream, and know how a rule moves from a draft to a baseline, how it is deleted, and where every version of it is kept. A **business rule** is a decision of the business, not of the software: _a member whose spend over the last twelve months reaches 5,000 is a platinum member_. It is written in plain words with the condition it fires on and what follows, the exceptions, worked examples of an input and what is expected, who has the authority to change it and the dates it is in force. Rules number on their own sequence (`CRM-BR-7`) whatever their kind, so a rule and a requirement never share a key, and every save of one is kept as a [version](/planning/revisions-and-versions), examples included. Requirements say what the system shall do; rules say what the business has decided, and the same rule is read by every requirement, story and test that touches it. ## Before you begin | You want to | You need | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Open the Business Rules page | `business-rule:view-any` on the project, which every default role carries. A client guest sees the agreed rules only. | | Add a rule | `business-rule:create`. Project managers and business analysts have it by default. | | Edit or move a rule | `business-rule:update`. | | Delete a rule | `business-rule:delete`. | | Name an owner | The person to be a member of the team. | | Name an authority | The stakeholder to be on the project's [stakeholder list](/projects/stakeholders). | The page lives under a project: open the project and choose **Business Rules** in the sidebar, badged with how many rules you may see. ## Read the rules [step] The table lists one rule per row: | Column | What it shows | | ------------- | ---------------------------------------------------------------------------------------------------- | | **Reference** | The rule's key, painted in the colour of its kind. | | **Rule** | The name, with the first line of the statement beneath it. Press the name to open the detail flyout. | | **Kind** | Constraint, derivation, computation, inference, action enabler or policy, with the kind's icon. | | **Status** | Draft, in review, approved, baselined or deprecated, as a badge. Hidden from a guest. | | **Priority** | The MoSCoW letter: M, S, C or W. | | **Owner** | The person who answers for the rule, or _Unassigned_. | | **In Force** | The dates the rule applies between, _Always_ when none, _Open-ended_ when it has no end. | | **Updated** | How long ago the record last changed. | | Row menu | **View Rule**, **Edit Rule**, **Move to**, **Activity**, **Delete Rule**. | Sort by reference, name, kind, status, priority, the start date or updated by pressing a column heading; press it again to flip the direction. The list opens in the project's own order. Above the table, narrow the list with the search box (words in the reference, the name or the authority), the **kind**, **status** and **priority** lists. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Add a rule [step] 1. Press **New Rule**. 2. Pick the **Kind** and the **Priority**. A constraint is the default; the kind says how the rule is read, tested and coded. 3. Type the **Name** and the **Statement**: what the rule is called and the rule in plain words, as the business would say it. These two are enough; Heimdall will propose the rest. 4. Write **When** the rule applies and **Then** what follows, each in one line, and the **Exceptions**. 5. Under _Worked examples_, press **Add Example** and type an input and what the rule gives for it; add one for the boundary and one for an exception. 6. Under _Authority and dates_, pick the **Stakeholder** who may change the rule, type the **Authority** it rests on and set the dates it is **In Force**. 7. Pick the **Owner** and press **Create Rule**. The rule takes the next number of the rules' sequence (the first rule of `CRM` is `CRM-BR-1`), starts as a draft, appears in the table at once, writes its first version with its examples inside it, and `business_rule.created` goes out to every endpoint subscribed to it. <Callout type="info" title="A rule without an example is argued about"> _Platinum from 5,000_ reads clear until someone asks about 4,999.99, about a refund, or about the day the threshold is crossed. Three examples, one at the boundary and one for an exception, settle the argument before the code is written, and the test generator will turn each one into a case. </Callout> ## Open a rule [step] Press a rule's name, or **View Rule** in its row menu, to open the detail flyout: the reference, kind, status and priority at the top, the name, then the statement, the condition and the action side by side, the exceptions, the worked examples in order, the stakeholder and the authority, the dates in force, the owner, the dates, and the **Versions** section with every save of the rule. From the flyout: - **Edit** opens the editor on this rule. - **Move to** changes the status in one step, offering only the moves the lifecycle allows. - **Restore this version** under any earlier version puts it back, examples included; see [revisions and versions](/planning/revisions-and-versions). - **Activity** opens the rule's history: every change, who made it and where it came from. - **Delete** opens the confirmation. A link such as `/projects/<id>/rules?show=<rule id>` opens the page with that rule's flyout already open; Heimdall's **View** buttons use it. ## Edit a rule [step] 1. Press **Edit Rule** in the row menu, or **Edit** in the flyout. 2. Change what you need, the example rows included: **Add Example** appends a row, the arrows reorder the rows and the cross removes one. The owner must be a member of the team, the stakeholder one of the project's, and the end date on or after the start. 3. Press **Save Changes**. The save writes a new version of the rule, examples included, and `business_rule.updated` goes out with the columns that changed, each with its old and new value. An edit that changes nothing writes nothing and announces nothing. The status does not change from the editor; it moves from the list or the flyout. ## Move a rule along its lifecycle [step] A rule reaches **In review** and **Approved** through its [approval](/planning/approvals) alone, the same way a [requirement](/planning/functional-requirements) does: **Submit for review** on the approval bar puts it in review, and the approvers' verdicts move it out, to approved when the request carries and back to where it stood when it is rejected or withdrawn. **Move to** in the row menu or the flyout offers the other moves the lifecycle allows from where the rule stands, and is refused while a request is open: | From | To | | -------------- | --------------------- | | **Draft** | Deprecated | | **In review** | Draft, deprecated | | **Approved** | Baselined, deprecated | | **Baselined** | Deprecated | | **Deprecated** | Draft | An approval stamps when the rule was agreed and at which version, and a client guest sees it from that moment on, through baselined, and loses it when it is deprecated. Every move writes a version with the reason _moved_ and `business_rule.status_changed` goes out with the status before and after; an approval writes one with the reason _approval_. <Callout type="info" title="Baselined is the freeze's badge"> A [baseline](/planning/baselines) that holds an approved rule moves it to baselined when the baseline is frozen and locks it: the editor, the status menu and the restore button refuse, naming the baseline, until an approved [change request](/planning/change-requests) unlocks it, which returns it to approved. The status menu still offers the move to baselined by hand, for a scope agreed outside the application; that badge carries no lock. </Callout> ## Delete a rule [step] **Delete Rule** in the row menu or **Delete** in the flyout opens the confirmation. Deleting removes the rule with every version it kept and its activity history; its number is never handed out again. `business_rule.deleted` goes out. Prefer **Deprecated** for a rule the business withdrew: it keeps the record and the reasoning. ## All the fields <Accordions> <Accordion title="Kind"> Required, _constraint_ by default, and free to change later because every rule numbers on the same sequence. Constraint, derivation, computation, inference, action enabler or policy; the [status reference](/reference/status-reference#business-rule-kind) says what each is for. The editor describes the chosen kind under the field. </Accordion> <Accordion title="Name"> Required, 3 to 200 characters. What everyone calls the rule, next to its reference, in the table, the requirements that cite it and Heimdall's answers. </Accordion> <Accordion title="Statement"> Optional rich text, and the heart of the record. The rule in plain words, as the business would say it; the client signs this off. Filtered to a safe subset when shown. </Accordion> <Accordion title="When and Then"> Optional, one line of up to 500 characters each. The condition the rule fires on and what follows when it does; the code and the test are written from them. Shown side by side in the flyout. </Accordion> <Accordion title="Exceptions"> Optional rich text. When the rule does not apply; every exception is a negative test case. </Accordion> <Accordion title="Worked examples"> Optional, up to twenty rows, each an input (required, up to 500 characters) and what the rule gives for it (up to 500 characters). Ordered with the arrows, kept inside every version of the rule, sent as `examples` over the API, and drafted one test case each once the test module lands. A row without an input is refused. </Accordion> <Accordion title="Stakeholder and Authority"> Optional. The stakeholder who may change the rule, from the project's stakeholders (an approval policy may name their seat), and the policy, regulation or role the rule rests on, in words up to 300 characters. Both are shown in the flyout; the authority is searchable. </Accordion> <Accordion title="In force from and until"> Optional dates; the end on or after the start. When the rule starts and stops applying, empty for always and open-ended. The table shows the span and the release notes will say which rules changed in a release. </Accordion> <Accordion title="Owner"> Optional. The person who answers for the rule; a member of the team. The owner's name is shown in the table and the flyout. </Accordion> <Accordion title="Priority"> Required, _should have_ by default. The MoSCoW priority: must have, should have, could have or won't have. The [status reference](/reference/status-reference#moscow-priority) says what each means. </Accordion> <Accordion title="Status, reference, number, version and approved"> Read-only in the editor. The status moves from the list or the flyout, and through the [approval bar](/planning/approvals) for the review states; the reference and the number are assigned when the rule is created; the version counts the approvals that carried and stays at zero until the first; approved is stamped by the approval, with the version that was read. The flyout also reads the names behind the ids: the owner's and the stakeholder's. </Accordion> </Accordions> ## What happens next - Every save, move and restore writes a [version](/planning/revisions-and-versions), examples included, and an entry in the project's [activity history](/workspace/activity-log). - `business_rule.created`, `business_rule.updated`, `business_rule.status_changed`, `business_rule.restored` and `business_rule.deleted` go out to the webhook endpoints subscribed to the `business_rule.*` family. - Requirements, stories, data entities and test cases will link to rules, the quality review will flag a story that contradicts a rule, and the test generator will draft one case per example, as those modules land. ## Heimdall on this page **Quality** in the flyout asks Heimdall to read the rule for its condition, action, exceptions, worked examples and authority, and for conflicts and overlaps with every other rule of the project, and to score it out of 100 with the findings to fix. See [Quality review](/planning/quality-review). Heimdall reads rules and their versions through the same tools the MCP server offers, proposes rules as drafts a person reviews when a story or a requirement implies one, and carries the open project as its context. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “Which rules have no worked example yet?” · “The returns story says refunds are accepted within thirty days; propose that as a rule with two examples.” · “Does CRM-BR-7 contradict anything in the loyalty epic?” The brief import will draft whole rulebooks into a review queue, and the quality review will keep asking for an example until each rule has one. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------- | ----------------------------------------------------------------------------------------- | | See the rules | `business-rule:view-any`, `business-rule:view`; a client guest sees the agreed rules only | | Add a rule | `business-rule:create` | | Edit or move a rule | `business-rule:update` | | Delete a rule | `business-rule:delete` | | Read or restore its versions | `business-rule:view` to read, `business-rule:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="Is this a requirement or a rule?"> A requirement says what the system shall do: _the system shall show a member their tier_. A rule says what the business has decided, whatever system does it: _platinum from 5,000_. Write the decision once as a rule and let every requirement, story and test that touches it cite the reference. </Accordion> <Accordion title="Why does a guest not see my draft?"> A draft is the analyst's working note. A client guest sees a rule once it is approved and from then on, so what the client reads is always what was agreed. </Accordion> <Accordion title="Can I change a rule's kind?"> Yes, from the editor at any time. Every rule numbers on the same sequence, so the reference stays and only the badge and the way the rule is read change. </Accordion> </Accordions> ## Related - [Quality review](/planning/quality-review) — the Quality panel in the flyout: Heimdall's score and findings, and the badge on the row - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [Functional requirements](/planning/functional-requirements) — what the system shall do, which the rules constrain - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [Status reference](/reference/status-reference#business-rule-kind) — every kind, status and priority - [`business_rule.*` webhook events](/webhooks/v1/events/business-rule) --- # Change Requests Source: https://alm-docs.labs.zuselab.dev/planning/change-requests By the end of this page you will know how a change to an agreed record is asked for and decided: how a change request is raised and numbered, how the records it touches and the baseline it changes are named, how its impact is written by an analyst or drafted by Heimdall and weighed by the council, how its approval unlocks exactly the records it names, how the change is seen through to verified, and what a client guest raises and reads. A change request is the project's answer to _the record was agreed, and now it must change_. It carries why (a new requirement, a clarification, a defect, a regulation, the client's wish, debt to repay, scope to drop), who asked, the frozen baseline it changes and the records it touches, and the impact on scope, schedule, effort, cost and risk with a level the approvers read first. Nothing frozen moves until the approvers agree; when they do, the baseline items the request names are unlocked, and only those. Each request is numbered from its own sequence (`CRM-CR-2`); a number is never handed out twice. ## Before you begin | To | You need | | ----------------------------------------- | ------------------------------------------------------------------------------------------ | | See the change requests | `change-request:view-any` and `change-request:view`; a client guest sees their own | | Raise one | `change-request:create`; a client guest raises one without the impact | | Change one, move it along | `change-request:update` | | Move one under analysis, write the impact | `change-request:analyse` | | Ask Heimdall for the impact | `change-request:analyse` and `ai:use`, on a project whose assists are on | | Submit it to the approvers | `change-request:approve` | | Approve or reject it | To be named by the project's **Change request** policy, or to be the stakeholder who asked | | Verify the change landed | `change-request:verify` | | Delete one that was never agreed | `change-request:delete` | A request needs no baseline: a change to a record nobody froze is still worth a reason, an impact and a verdict. A request against a frozen [baseline](/planning/baselines) is the one that unlocks. ## Open the page [step] **Baselines** in the project's sidebar, then the **Change Requests** tab. The badge on the sidebar entry counts the requests still open: everything but the rejected, verified and closed ones. The list shows each request's reference, its title with the baseline it is raised against and the stakeholder who asked, its reason, its priority, its impact level (or _Not yet_), its status and when it last changed. Sort by number, title, reason, priority, impact, status or the last change; filter by reason or status, or search by reference or title; **Clear Filters** drops every filter. ## Raise a request [step] Press **New Change Request**. The editor is in three parts: - **The Change**: a **Title**, the **Reason** it is raised for, its **Priority**, the **Baseline** it is raised against (the frozen baselines of the project; leave it empty when none applies), the stakeholder who asked under **Asked by** (the client's people from [stakeholders](/projects/stakeholders); that seat co-signs the impact once an account is linked to it), and a **Description** of what is asked for and why, in the words of the person who asked. - **Records Touched**: the epics, stories, requirements, rules and use cases the change reaches. With a baseline named, its records are offered; without one, every approved record of the project is. Each pick becomes a link from the request; an approved request unlocks the ones its baseline holds. - **Impact**: the **Impact level**, the **Schedule impact** in days, the **Effort** in hours, the **Cost**, and the prose on **Impact on scope** and **Impact on risk**. Leave what you do not know; the analyst fills it later, or Heimdall does. **Raise Change Request** writes it as **Submitted**, numbered from the project's change requests sequence, with you as the person who raised it, and the people watching the project hear `change_request.created`. A title and a reason are enough. The impact can wait for the analyst; the records can be named later, as long as they are named before the approvers decide, since the approval unlocks what is named at that moment. ## Analyse the impact [step] Open the request's flyout. **Move to → Under analysis** says the analyst has it. Then either write the impact in the editor (**Edit**), or press **Analyse with Heimdall**: Heimdall reads the request, the impact you already wrote, the baseline and every record it names with what links to each, and answers with a summary, the scope and the risk as prose, a schedule in days, an effort in hours, a level, the records it believes the request forgot and the questions to put to the client before deciding. The draft lands under **Heimdall's reading** in the flyout; the numbers and the prose fill only the fields you left empty, and never replace a number you wrote. Ask again after you edit and Heimdall weighs what you wrote against its own reading. A request still only **Submitted** moves to **Under analysis** when Heimdall reads it, because it has now been analysed. How the reading is made, what it costs and where it stops is on [impact analysis](/copilot/impact-analysis). For a change that deserves more than one lens, press **Convene the Council**: the [council](/copilot/council) sits on its **Change impact** moment with Odin, Loki, Týr and Frigg by default, each reading the same brief through their own lens, and Heimdall's verdict ranks what they found. The sitting appears under **The Council** in the flyout as the voices speak; the button returns when the sitting is done. <Callout type="info" title="The effort and the cost stay on the team's side"> A client guest reads the request's level, its schedule impact, its scope and its risk, never the hours, the cost or Heimdall's draft. Those are the team's working numbers until the approvers decide; the application's services keep them back, not just the screen. </Callout> ## Submit it to the approvers [step] With the impact written, the **Approval** bar offers **Submit for review** to anyone with `change-request:approve`. The request moves to **Awaiting approval**; the approvers are the people the project's **Change request** policy names (the project manager role by default, under the **Every approver** rule) and, when a stakeholder asked for the change, that stakeholder through the guest account linked to their seat. Each is told, each decides once, and the request waits. What the approval does when it carries: - The request becomes **Approved** at version 1, stamped with the time. - Every baseline item the request names is unlocked, with the request recorded as the reason; the rest of the baseline stays frozen. - A requirement or a rule among them that no other frozen baseline holds moves from **Baselined** back to **Approved**, so its editor opens again; the next edit marks it changed since its approval and the approval bar asks for a new submission. An epic, a story or a use case is simply free. - `change_request.status_changed` goes out beside the approval's events. A rejection settles the request as **Rejected** with the approver's words under **Why it was refused**; the baseline stands as it was. A rejected request goes back **Under analysis** by hand when the team wants another round, and is submitted again as a new request. A request for changes holds it until it is edited and resubmitted. ## See the change through [step] An approved request moves by hand along the rest of its lifecycle from the row menu or the flyout: **Implementing** while the team makes the change, **Implemented** when it is made, **Verified** when someone with `change-request:verify` confirms it landed as asked (the flyout names who and when), and **Closed** at the end. A request can also be closed from **Submitted** or **Under analysis** when it is withdrawn before anyone decides it. The moves into and out of the gate (**Awaiting approval**, **Approved**, **Rejected**) are the approval engine's and the menu does not offer them; while a request is under review, the menu is closed until it is withdrawn. ## Edit and delete [step] A request is edited until the approvers decide it: **Edit** reopens the editor for the words, the records touched and the impact while it is submitted, under analysis or awaiting approval; every save is a version. An approved or rejected request no longer changes. **Delete Change Request** removes a request that was never agreed, with its links and its versions; its number is never handed out again. A request past its gate is closed rather than deleted. ## All the fields <Accordions> <Accordion title="Title and description (ChangeRequestForm.title, ChangeRequestForm.description)"> The title is 3 to 200 characters, shown beside the reference everywhere. The description is rich text up to 20,000 characters, optional: what is asked for and why, in the words of the person who asked. Heimdall's inline assist drafts and improves it for staff. </Accordion> <Accordion title="Reason and priority (ChangeRequestForm.reason, ChangeRequestForm.priority)"> The reason is one of new requirement, clarification, defect, regulatory, client request, technical debt or scope reduction; default new requirement. The priority runs from lowest to highest; default medium. Both colour the list and are filters on it. </Accordion> <Accordion title="Baseline and asked by (ChangeRequestForm.baseline_id, ChangeRequestForm.stakeholder_id)"> The baseline must be one of the project's, optional; its records are the ones offered under Records Touched and the ones an approval unlocks. The stakeholder must be one of the project's, optional; a request raised by a client guest takes their own seat when their account is linked to one. A guest never sees the picker. </Accordion> <Accordion title="Records touched (ChangeRequestForm.affected, ChangeRequestForm.affected_rows)"> Up to 200 records, each an epic, a story, a requirement, a business rule or a use case of the project, named by family and id; the baseline's records when a baseline is named, every approved record otherwise. Kept as links from the request and shown in the flyout by key and title, a hidden one as _(hidden)_. </Accordion> <Accordion title="Impact level, schedule, effort and cost (ChangeRequestForm.impact_level, impact_schedule_days, impact_effort_hours, impact_cost)"> The level is low, medium or high, optional until judged. The schedule impact is a whole number of days between -3,650 and 3,650, negative when the change shortens the plan. The effort is hours from 0 to 999,999.99; the cost a number from 0 to 9,999,999,999.99 in the project's currency. A client guest neither sees nor writes the effort and the cost; Heimdall fills an empty number and never overwrites a written one. </Accordion> <Accordion title="Impact on scope and on risk (ChangeRequestForm.impact_scope, ChangeRequestForm.impact_risk)"> Rich text up to 20,000 characters each, optional. What the change touches, record by record; what it risks: regressions, security, the dates already promised. Heimdall's inline assist drafts either for staff, and its impact analysis fills an empty one. </Accordion> <Accordion title="Heimdall's reading (ChangeRequestForm.ai_impact_draft, ChangeRequestForm.analysed_at)"> Read-only. Heimdall's draft as one block: the summary, the scope, the risk, the records it believes are touched and the questions to ask before deciding, with when it was last read. Staff only; replaced by the next reading. </Accordion> <Accordion title="Status, decision, verification and version (ChangeRequestForm.status, decision_note, verifier_name, verified_at, version, approved_at)"> Read-only. The status moves by hand through submitted, under analysis, implementing, implemented, verified and closed, and through the approval engine into awaiting approval, approved and rejected. The decision note is the approver's reason on a rejected request. The verifier and the time are stamped by the move to verified. The version is 0 until approved and 1 from the approval, with its time. </Accordion> <Accordion title="Reference, number, names and the clocks (ChangeRequestForm.reference, number, requester_name, stakeholder_name, baseline_name, id, created_at, updated_at)"> Read-only. The reference spells the project's key, `CR` and the number from the change requests' own sequence; who raised it, who asked and the baseline named are shown by name; the id and the clocks are the record's own, in your timezone. </Accordion> </Accordions> ## What happens next - Raising emits `change_request.created`; every edit `change_request.updated` with what changed, the records touched included as the keys before and after; a move by hand and the approval engine's moves `change_request.status_changed`. - Heimdall's reading emits `change_request.analysed` with the level and how many records it named, and files its cost on the team's [AI ledger](/copilot/usage-and-budgets) under impact analysis. - The approval emits `approval.requested`, the verdicts and the outcome; the unlock shows on the baseline's records as **Unlocked** with the request's reference. - Deleting emits `change_request.deleted`; putting an earlier version back `change_request.restored`. - A request raised by a client guest, or asked through their seat, appears in their list and their notifications; the rest of the project's requests do not. ## Heimdall on this page Heimdall raises, reads and analyses change requests in conversation: _"The client wants vouchers on renewals too; raise a change request against the signed scope touching CRM-FR-12 and work out the impact"_ raises it, names the record and reads the impact in one go; _"Which change requests are waiting on Pat?"_ reads the inbox. Heimdall never submits a request to the approvers on its own and never decides one. The [impact analysis](/copilot/impact-analysis) page explains what the reading is given and where it stops, and the [council](/copilot/council)'s change impact moment is the reading to ask for before a high-impact request goes to the client. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | --------------------------------------- | ------------------------------------------------------------------------------------------------------- | | See the requests and open one | `change-request:view-any`, `change-request:view`; a client guest sees the ones they raised or asked for | | Raise one | `change-request:create`; a client guest without the impact | | Change one; move it along by hand | `change-request:update` | | Move one under analysis; ask Heimdall | `change-request:analyse`; Heimdall also needs `ai:use` and the project's assists on | | Convene the council | `ai:use`, as staff, on a project whose assists are on | | Submit it to the approvers | `change-request:approve`, as staff | | Approve or reject | Named by the policy or the stakeholder who asked; `approval:decide` over the API | | Verify | `change-request:verify` | | Delete one never agreed | `change-request:delete` | | Read and restore the request's versions | `change-request:view`, `change-request:update`; staff only | ## Frequently asked **Does approving a request change the records for me?** No. It unlocks them. The requirement, the story or the rule reads as before; you make the change in its own editor, which opens again, and submit the record for its own approval when the change is written. The request moves to implementing, implemented and verified as that work happens. **The approved request named three records but only two were unlocked.** The third was not held by the baseline the request names: a record held by another frozen baseline, or by none, is not the request's to unlock. Name the right baseline, or raise a second request against the other one. **Why does the stakeholder who asked also have to approve?** Because the request speaks for them. Their seat is added to the approvers beside the policy's people, so the client co-signs the impact of what they asked for. A seat with no linked account cannot decide; invite the person first, or leave the seat off the request. **Can a client guest raise a change request?** Yes, with a title, a reason, the baseline and the records; the impact fields are not theirs. The request appears in their list with its level and schedule once the team wrote them, and they hear about the verdict. ## Related - [Baselines](/planning/baselines) — what a request unlocks and how it was frozen - [Approvals](/planning/approvals) — the engine, the policies and the inbox - [Impact analysis](/copilot/impact-analysis) — what Heimdall reads and how it answers - [The council](/copilot/council) — the change impact moment - [Signing off the scope](/guests/signing-off-scope) — the client's side of a baseline and a request - [Status reference](/reference/status-reference#change-request-status) — every reason, status and impact level --- # Comments and mentions Source: https://alm-docs.labs.zuselab.dev/planning/comments-and-mentions By the end of this page you will have asked a question on a requirement and had it answered under it, told a teammate by naming them, kept a note from the client, closed the thread when the question was settled, and chosen which artefacts you hear about; you will also know what a client guest sees of the conversation and what each comment announces. A **comment** is a note on one artefact: a question on a requirement, a finding on a story, a decision taken in the margin of a RAID item. Comments live in the **Discussion** panel at the foot of the artefact's flyout, under its links, on every page that has one: epics, work items, requirements, business rules, use cases, RAID items, decisions, data entities and glossary terms. A comment answers at most one earlier comment, so a thread is one level deep and reads as a conversation rather than a tree. A comment is either open to everyone who sees the artefact or **internal**, which a client guest never reads. Naming someone with `@` tells them; **following** an artefact tells you of every comment on it. The first comment of a thread can be **resolved** when the question is settled, and reopened when it comes back. ## Before you begin | You need | Where it comes from | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | An artefact to discuss | Anything from the [planning](/planning) pages or the [dictionary](/projects/glossary). | | `comment:view-any` | To see the panel; every role has it, a client guest for the open comments only. | | `comment:create` | To post; every staff role has it, a client guest when the project's **Guests may comment** setting is on. | | `comment:internal` | To post an internal comment; project managers, analysts, developers, QA and release managers have it. | | `comment:update`, `comment:delete` | To edit, resolve or delete someone else's comment; your own need nothing beyond `comment:create`. | ## Read the discussion [#read] 1. Open the artefact's page and open the artefact: its flyout opens from the row's menu, from **View**, or from a `?show=` link someone sent you. 2. Scroll to **Discussion**, under the artefact's links. Threads are listed oldest first, each with its replies indented under it. Every comment shows who wrote it and when (the full date on hover), an **Internal** badge when the client never sees it, a **Resolved by** badge on a settled thread, and **(edited)** when the words were rewritten. The panel's heading counts the people following the artefact. ## Post a comment [#post] 1. Write in **Add a Comment** at the foot of the panel. Bold, italics, lists and links are there; paste a key such as `CRM-FR-12` to name another artefact. 2. Turn **Internal** on when the client must not read it. The switch is there only for roles that may post internally. 3. Press **Post**. The comment appears at the end of the list, a toast confirms it, you become a follower of the artefact if you were not one, the people you named and the other followers are told, and `comment.created` goes out. ## Mention someone [#mention] Type `@` followed by the person's full name, `@Jane Doe`, anywhere in the comment. A first name alone, `@Jane`, works when exactly one Jane may read the comment; a shared first name names nobody rather than someone at random. Case, bold and links around the name change nothing. The people named receive a **mention** alert, routed under the Mentions class of their [notification settings](/account/notifications), wherever the comment is; a client guest is told only of open comments, never of internal ones, even when named. ## Reply in a thread [#reply] 1. Press **Reply** under the first comment of a thread. A chip above the editor says whom you are answering; **Start a new thread instead** drops it. 2. Write and press **Post**. A reply takes the audience of its thread: under an internal comment it is internal whatever the switch says, and the switch is disabled while you reply. Replies cannot be replied to, so a long discussion stays one conversation under one question. ## Resolve a thread [#resolve] Press **Resolve** under the first comment when the question is settled. The thread is marked resolved with your name, its text dims, and `comment.resolved` goes out. **Reopen** brings it back, with `comment.reopened`. Only the first comment of a thread is resolved; its replies follow it. The author of the thread and anyone with `comment:update` may resolve and reopen. ## Edit or delete a comment [#edit] - **Edit** opens your own comment in the editor; **Save** rewrites it and marks it **(edited)**, anyone newly named is told, and `comment.updated` goes out. **Cancel** keeps the old words. - **Delete** asks once, inline, and then removes the comment; the replies under a first comment go with it, and one `comment.deleted` goes out for the comment you named. **Keep** backs out. You edit and delete your own comments; `comment:update` and `comment:delete` let a moderator do the same for anyone's. ## Follow an artefact [#follow] Press **Follow** in the panel's heading to hear of every comment posted on the artefact; the button reads **Following** once you do, and pressing it again stops. Commenting follows the artefact for you without asking. A follower's alert is routed under the Planning class of their [notification settings](/account/notifications) for the planning artefacts; a client guest who follows hears of open comments only. Followers are told of comments, not of edits, resolutions or deletions, which the artefact's own events carry. ## All the fields <Accordions> <Accordion title="Add a comment"> The words, with bold, italics, lists and links. Up to 20,000 characters; never empty. Name people with `@` and their full name to tell them. The words are kept as you wrote them and cleaned of anything that is not text when read back, so a pasted script never runs. </Accordion> <Accordion title="Internal"> Off by default. On, the comment is kept from client guests: it never appears in their panel, their API listing or their alerts, and a guest named in it is not told. Needs `comment:internal`; a reply takes the audience of its thread and the switch is disabled while you reply. </Accordion> <Accordion title="Your reply"> The same editor, shown with a chip naming the person whose thread you answer. A reply is one level deep: it answers a first comment, never another reply. </Accordion> </Accordions> ## Who hears what | Event | Who is told | Routed under | | -------------------------------- | ------------------------------------------------------------------- | ------------ | | A comment names you | You, if you may read the comment | Mentions | | A comment on an artefact you follow | Every follower but the author and the people named | Planning | | A comment is edited | Only someone newly named | Mentions | | A thread is resolved or reopened | Nobody; the events carry it | | A client guest is a follower like anyone else, but hears of open comments only. ## What happens next - `comment.created`, `comment.updated`, `comment.resolved`, `comment.reopened` and `comment.deleted` go out with the artefact, the author, the audience and the thread, never the words; see the [`comment.*` events](/webhooks/v1/events/comment). An internal comment is announced like any other, because webhook endpoints are the team's. - A comment is not a [version](/planning/revisions-and-versions) of the artefact: posting, editing and resolving change nothing on the record itself. The activity log keeps each comment's own changes, so an edit is never silent. - Heimdall and an agent connected over MCP read the discussion before they draft or act, so a question answered in the margin reaches them without being pasted into the artefact. ## Managing - **A moderator** with `comment:update` edits and resolves anyone's comment; with `comment:delete` deletes anyone's. Both are given to project managers by default. - **Guests may comment** is a [project setting](/projects/project-settings), on by default; off, a client guest reads the open comments and posts nothing. Internal comments stay internal whatever the switch says. - **An artefact deleted** takes its comments with it. - **A deleted account** leaves its comments in place, with the author shown as someone. ## Heimdall on this page Heimdall reads the discussion of an artefact when it drafts for it or answers about it, and an agent connected over MCP posts its own comments with [`comment_on_item`](/mcp/v1/tools/planning/comment-on-item) when it has a question, a finding or a decision to record: before it moves a status, after it links a pull request, when a criterion cannot be verified. Neither edits, resolves or deletes what anyone said, and neither follows anything: the conversation stays with the people in it. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------------------- | ------------------------------------------------------------------------------------------ | | See the discussion | `comment:view-any` and `comment:view`; a client guest sees the open comments only | | Post a comment | `comment:create`; a client guest only when the project's **Guests may comment** is on | | Post an internal comment | `comment:internal`; never a client guest | | Edit, resolve or delete your own comment | `comment:create` | | Edit or resolve anyone's comment | `comment:update` | | Delete anyone's comment | `comment:delete` | | Follow an artefact | Seeing it | ## Frequently asked <Accordions> <Accordion title="Why was the person I named not told?"> A mention needs the full name, or a first name that only one person who may read the comment has. Check the spelling against the team's members page, and remember that a client guest is never told of an internal comment, even when named. </Accordion> <Accordion title="Can I reply to a reply?"> No. A thread is one question and the answers under it; reply to the first comment and name the person you are answering. If the discussion has moved on, start a new thread. </Accordion> <Accordion title="Does the client see my comments?"> A client guest sees the open comments on the artefacts they may read, which are the approved ones. Turn **Internal** on for anything the client must not read; a reply under an internal comment is internal too. </Accordion> <Accordion title="Why am I getting alerts for an artefact I never followed?"> Commenting follows the artefact for you. Press **Following** in the panel's heading to stop; you will still be told when someone names you. </Accordion> </Accordions> ## Related - [Notifications](/account/notifications) — where the Mentions and Planning alerts go - [Traceability](/planning/traceability) — the links panel above the discussion - [Project settings](/projects/project-settings) — whether guests may comment - [Revisions and versions](/planning/revisions-and-versions) — what a comment is not - [`comment.*` webhook events](/webhooks/v1/events/comment) --- # Decisions Source: https://alm-docs.labs.zuselab.dev/planning/decisions By the end of this page you will have recorded the choices a project made as decisions someone can read in a year and still understand; know every field a decision carries and what reads it downstream; and know how a decision moves from proposed to accepted, how a newer decision supersedes an older one, how a decision is deleted, and where every version of it is kept. A **decision** is the record of a choice: _keep the loyalty ledger in Postgres_. It keeps the **context** the choice was made in, the **options** that were on the table with what spoke for and against each, what was **decided**, the **consequences** that follow, who decided and when, and the earlier decision it **supersedes**. Decisions number on their own sequence (`CRM-DEC-5`) and carry a kind (architecture, product, process, vendor or security) that says who should sit at the table. The log is internal: a client guest never sees it. Every save of a decision is kept as a [version](/planning/revisions-and-versions). The page is the second tab of the RAID family, beside the [RAID log](/planning/raid-log), because the risks and the decisions are read in the same meeting. ## Before you begin | You want to | You need | | ------------------------ | ----------------------------------------------------------------------------------------------------- | | Open the Decisions tab | `decision:view-any` on the project, which every staff role carries. A client guest is refused at the door. | | Record a decision | `decision:create`. Project managers, business analysts and developers have it by default. | | Edit or move a decision | `decision:update`. | | Delete a decision | `decision:delete`. | | Name a decider | The person to be a member of the team. | The page lives under a project: open the project, choose **RAID Log** in the sidebar and press the **Decisions** tab. ## Read the decisions [step] The table lists one decision per row: | Column | What it shows | | -------------- | -------------------------------------------------------------------------------------------------- | | **Reference** | The decision's key, painted in the colour of its kind. | | **Decision** | The title, with the first line of what was decided beneath it. Press the title to open the flyout. | | **Kind** | Architecture, product, process, vendor or security, with the kind's icon. | | **Status** | Proposed, accepted, rejected, superseded or deprecated, as a badge. | | **Decided** | The day it was accepted, or _Not yet_. | | **Supersedes** | The reference of the earlier decision it replaces, when it names one. | | **Updated** | How long ago the record last changed. | | Row menu | **View Decision**, **Edit Decision**, **Move to**, **Activity**, **Delete Decision**. | Sort by reference, title, kind, status, the day decided or updated by pressing a column heading; press it again to flip the direction. The list opens in the project's own order. Above the table, narrow the list with the search box (words in the reference or the title), the **kind** and **status** lists. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Record a decision [step] 1. Press **New Decision**. 2. Pick the **Kind**, and under **Supersedes** the earlier decision this one replaces, if any. 3. Type the **Title**: what was chosen, in one line. This alone is enough; Heimdall will propose the rest. 4. Write the **Context**: the situation and the forces that made a decision necessary. 5. Under _Options weighed_, press **Add Option** once per option and type what it is, its **Pros** and its **Cons**; put the chosen one first. 6. Write what was **Decided** and the **Consequences**. 7. Pick the **Deciders** from the team and press **Record Decision**. The decision takes the next number of the decisions' sequence (the first decision of `CRM` is `CRM-DEC-1`), starts as proposed, appears in the table at once, writes its first version with its options inside it, and `decision.created` goes out to every endpoint subscribed to it. <Callout type="info" title="A decision with one option was never a decision"> Write down what else was on the table, even the option everyone dismissed in a minute, with the reason it lost. The next person to ask _why not the event store?_ reads the answer instead of reopening the question. </Callout> ## Open a decision [step] Press a decision's title, or **View Decision** in its row menu, to open the detail flyout: the reference, kind and status at the top, the title, then the context, the options weighed in order with their pros and cons, what was decided and the consequences side by side, the deciders, the day decided, the decision it supersedes, the dates, and the **Versions** section with every save of the decision. From the flyout: - **Edit** opens the editor on this decision. - **Move to** changes the status in one step, offering only the moves the lifecycle allows. - **Restore this version** under any earlier version puts it back, options included; see [revisions and versions](/planning/revisions-and-versions). - **Activity** opens the decision's history: every change, who made it and where it came from. - **Delete** opens the confirmation. A link such as `/projects/<id>/raid/decisions?show=<decision id>` opens the page with that decision's flyout already open; Heimdall's **View** buttons use it. ## Edit a decision [step] 1. Press **Edit Decision** in the row menu, or **Edit** in the flyout. 2. Change what you need, the option rows included: **Add Option** appends a row, the arrows reorder the rows and the cross removes one. Every decider must be a member of the team, and a decision cannot supersede itself or one of another project. 3. Press **Save Changes**. The save writes a new version of the decision, options included, and `decision.updated` goes out with the columns that changed, each with its old and new value. An edit that changes nothing writes nothing and announces nothing. The status does not change from the editor; it moves from the list or the flyout. ## Move a decision along its lifecycle [step] **Move to** in the row menu or the flyout offers the moves the lifecycle allows from where the decision stands: | Status | Meaning | May move to | | -------------- | ------------------------------------------------------ | ---------------------- | | **Proposed** | Put forward with its options; not yet decided. | Accepted, rejected | | **Accepted** | Decided and in force; the team builds on it. | Superseded, deprecated | | **Rejected** | Turned down; kept so the reasoning is not lost. | Proposed | | **Superseded** | Replaced by a later decision that names it. | Accepted | | **Deprecated** | No longer applies and nothing replaced it. | Proposed | A move into **Accepted** stamps the day it was decided (when none was set) and, when the decision names an earlier one under _Supersedes_, moves that one to **Superseded** in the same breath, so the log never holds two decisions in force on the same question. Every move writes a version with the reason _moved_ and `decision.status_changed` goes out with the status before and after, once for each decision that moved. <Callout type="info" title="Until the approvals land"> A project manager, an analyst or a developer accepts a decision by hand today. The approval module will let the deciders named on the record accept it themselves, with a signature on the version they accepted. </Callout> ## Delete a decision [step] **Delete Decision** in the row menu or **Delete** in the flyout opens the confirmation. Deleting removes the decision with every version it kept and its activity history; a later decision that named it keeps its own record and loses the link; its number is never handed out again. `decision.deleted` goes out. Prefer **Deprecated** for a decision that no longer applies: it keeps the reasoning. ## All the fields <Accordions> <Accordion title="Kind"> Required, _architecture_ by default, and free to change later because every decision numbers on the same sequence. Architecture, product, process, vendor or security; the [status reference](/reference/status-reference#decision-kind) says what each is for and who should sit at the table. </Accordion> <Accordion title="Title"> Required, 3 to 200 characters. What was chosen, in one line, next to the reference in the table, the context packs and Heimdall's answers. </Accordion> <Accordion title="Context"> Optional rich text. The situation and the forces at play: what made a decision necessary. Filtered to a safe subset when shown. </Accordion> <Accordion title="Options weighed"> Optional, up to ten rows, each a title (required, 2 to 200 characters) with what spoke for it and against it (up to 1,000 characters each). Ordered with the arrows, the chosen one first by convention; kept inside every version of the decision and sent as `options` over the API. A row without a title is refused. </Accordion> <Accordion title="Decision and Consequences"> Optional rich text. What was decided, in plain words the team builds on, and what follows: what becomes easier, what becomes harder, what must be done. Shown side by side in the flyout. </Accordion> <Accordion title="Deciders"> Optional, up to twenty people from the team. The people who made the call, kept as their ids and shown by name in the flyout, so the reasoning has owners; the approvals will let them accept the record themselves. </Accordion> <Accordion title="Supersedes"> Optional. The earlier decision of the same project this one replaces. Accepting this decision moves that one to superseded; deleting that one clears the link. The table and the flyout show its reference. </Accordion> <Accordion title="Status, reference, number and decided on"> Read-only in the editor. The status moves from the list or the flyout; the reference and the number are assigned when the decision is recorded; the day decided is stamped by the move into accepted. </Accordion> </Accordions> ## What happens next - Every save, move and restore writes a [version](/planning/revisions-and-versions), options included, and an entry in the project's [activity history](/workspace/activity-log). - `decision.created`, `decision.updated`, `decision.status_changed`, `decision.restored` and `decision.deleted` go out to the webhook endpoints subscribed to the `decision.*` family. - The context pack of a story will carry the accepted decisions it builds on, epics, requirements and components will link to decisions, and Odin in the council will cite them when a design or a change request is reviewed, as those modules land. ## Heimdall on this page Heimdall reads the decisions and their versions through the same tools the MCP server offers, records a decision as proposed when a conversation, a story or a design settles a choice, and carries the open project as its context. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “Which decisions are still proposed, and who should decide them?” · “We agreed in the stand-up to queue the digests through Redis; record that as an architecture decision with the options we discussed.” · “What did CRM-DEC-5 replace, and why?” The brief import will draft the decisions a brief already implies into a review queue, and the meeting notes extraction will record the decisions a meeting took. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------- | --------------------------------------------------------------------------------- | | See the decisions | `decision:view-any`, `decision:view`; never a client guest | | Record a decision | `decision:create` | | Edit or move a decision | `decision:update` | | Delete a decision | `decision:delete` | | Read or restore its versions | `decision:view` to read, `decision:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="Is this a decision or a business rule?"> A business rule is a decision of the business about how it works: _platinum from 5,000_. A decision here is a choice the project made about how to build or run the system: _the ledger lives in Postgres_. Rules are signed off by the client; decisions are owned by the team. </Accordion> <Accordion title="What happens to the old decision when I accept a new one?"> When the new decision names it under _Supersedes_, accepting the new one moves the old one to superseded and announces both moves. The old record stays readable, so the history of the question is one chain of references. </Accordion> <Accordion title="Can I reopen a rejected decision?"> Yes: move it back to proposed, change what needs changing and accept it. Every move is a version, so the first rejection and its reasoning are never lost. </Accordion> </Accordions> ## Related - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [RAID log](/planning/raid-log) — the risks and dependencies a decision answers to, on the first tab - [Business rules](/planning/business-rules) — the decisions of the business, as opposed to the team's - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [Status reference](/reference/status-reference#decision-kind) — every kind and status - [`decision.*` webhook events](/webhooks/v1/events/decision) --- # Epics Source: https://alm-docs.labs.zuselab.dev/planning/epics By the end of this page you will have written down a project's epics, know every field an epic carries and what reads it downstream, and know how an epic moves from a draft to done, how it is deleted, and where every version of it is kept. An **epic** is a body of work the client recognises: "loyalty tiers", "checkout redesign", "the supplier portal". It is the scope a release is cut from and the parent of every story, so it is written before the stories are, in the client's words, with the goal, the value and the measures that say when it is done. An epic takes the next number of the project's shared tracker sequence, so its reference (`CRM-12`) sits in the same series as the stories that will hang under it, and every save of it is kept as a [version](/planning/revisions-and-versions). ## Before you begin | You want to | You need | | ------------------- | ------------------------------------------------------------------------------------------------------------ | | Open the Epics page | `epic:view-any` on the project, which every default role carries. A client guest sees the agreed epics only. | | Add an epic | `epic:create`. Project managers and business analysts have it by default. | | Edit, move an epic | `epic:update`. | | Delete an epic | `epic:delete`. | | Name an owner | The person to be a member of the team. | The page lives under a project: open the project and choose **Epics** in the sidebar, badged with how many epics you may see. ## Read the epics [step] The table lists one epic per row: | Column | What it shows | | ------------- | -------------------------------------------------------------------------------------------- | | **Reference** | The epic's key, painted in the epic's colour. | | **Epic** | The title, with the goal beneath it. Press the title to open the detail flyout. | | **Status** | Draft, in review, approved, in progress, done or cancelled, as a badge. Hidden from a guest. | | **Priority** | The MoSCoW letter: M, S, C or W. | | **Owner** | The person who answers for the epic, or _Unassigned_. | | **Due** | The date agreed, when there is one. | | **Updated** | How long ago the record last changed. | | Row menu | **View Epic**, **Edit Epic**, **Move to**, **Activity**, **Delete Epic**. | Sort by reference, title, status, priority, due date or updated by pressing a column heading; press it again to flip the direction. The list opens in the project's own order. Above the table, narrow the list with the search box (words in the reference, the title or the goal), the **status** list and the **priority** list. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Add an epic [step] 1. Press **New Epic**. 2. Type the **Title** and the **Goal**: what the epic is called and, in one sentence, what it achieves. These two are enough; the rest can follow, and Heimdall will draft them from the brief once the brief import lands. 3. Write the **Summary**, the scope in prose, and under _Why it matters_ the **Business Value**, the **Hypothesis** the epic bets on and the **Success Metrics** that will test it. 4. Pick the **Owner** and the **Priority**, set the **Starts** and **Due** dates if they are agreed, and a **Colour** if you want the epic to stand out on the board and the roadmap. 5. Press **Create Epic**. The epic takes the next number of the project (the first epic of `CRM` is `CRM-1`), starts as a draft, appears in the table at once, writes its first version, and `epic.created` goes out to every endpoint subscribed to it. <Callout type="info" title="One sequence for the tracker"> Epics, stories, tasks and bugs share one number series per project, so `CRM-12` is always one thing and a reference in a commit or a conversation never needs a type. A number is never reused, even after a delete. </Callout> ## Open an epic [step] Press an epic's title, or **View Epic** in its row menu, to open the detail flyout: the reference, status and priority at the top, then the summary, the value, the hypothesis and the measures, the owner, the dates, and the **Versions** section with every save of the epic. From the flyout: - **Edit** opens the editor on this epic. - **Move to** changes the status in one step, offering only the moves the lifecycle allows. - **Restore this version** under any earlier version puts it back; see [revisions and versions](/planning/revisions-and-versions). - **Activity** opens the epic's history: every change, who made it and where it came from. - **Delete** opens the confirmation. A link such as `/projects/<id>/epics?show=<epic id>` opens the page with that epic's flyout already open; Heimdall's **View** buttons use it. ## Edit an epic [step] 1. Press **Edit Epic** in the row menu, or **Edit** in the flyout. 2. Change what you need. The owner must be a member of the team and the due date on or after the start. 3. Press **Save Changes**. The save writes a new version of the epic and `epic.updated` goes out with the columns that changed, each with its old and new value. An edit that changes nothing writes nothing and announces nothing. The status does not change from the editor; it moves from the list or the flyout. ## Move an epic along its lifecycle [step] An epic reaches **In review** and **Approved** through its [approval](/planning/approvals) alone: **Submit for review** on the approval bar puts it in review, and the approvers' verdicts move it out, to approved when the request carries and back to where it stood when it is rejected or withdrawn. **Move to** in the row menu or the flyout offers the other moves the lifecycle allows from where the epic stands, and is refused while a request is open: | From | To | | --------------- | ---------------------- | | **Draft** | Cancelled | | **In review** | Draft, cancelled | | **Approved** | In progress, cancelled | | **In progress** | Done, cancelled | | **Done** | In progress | | **Cancelled** | Draft | An approval stamps when the epic was agreed and at which version, and a client guest sees the epic from that moment on, through in progress and done, and loses it when it is cancelled. Every move writes a version with the reason _moved_ and `epic.status_changed` goes out with the status before and after; an approval writes one with the reason _approval_. ## Delete an epic [step] **Delete Epic** in the row menu or **Delete** in the flyout opens the confirmation. Deleting removes the epic with every version it kept and its activity history; the work items under it stay in the [backlog](/planning/backlog) and lose their epic, and its number is never handed out again. `epic.deleted` goes out. Prefer **Cancelled** for an epic the client dropped: it keeps the record and the reasoning. ## All the fields <Accordions> <Accordion title="Title"> Required, 3 to 160 characters. What everyone calls the epic, next to its reference, in the table, the roadmap, the release notes and Heimdall's answers. </Accordion> <Accordion title="Goal"> Optional, up to 300 characters. One sentence on what the epic achieves. Shown under the title in the table and read by the roadmap and the release notes. </Accordion> <Accordion title="Summary"> Optional rich text. The scope in prose: what the epic covers. Stories are drafted from it and the client reads it once the epic is agreed. Filtered to a safe subset when shown. </Accordion> <Accordion title="Business value"> Optional rich text. Why the client wants it; the priority is argued from it. </Accordion> <Accordion title="Hypothesis"> Optional, up to 500 characters. The bet the epic makes, if it makes one; the measures test it. </Accordion> <Accordion title="Success metrics"> Optional rich text. The numbers that say the epic is done, read when it is reviewed after launch. </Accordion> <Accordion title="Owner"> Optional. The person who answers for the epic; a member of the team. The owner's name is shown in the table and the flyout. </Accordion> <Accordion title="Priority"> Required, _should have_ by default. The MoSCoW priority: must have, should have, could have or won't have. The [status reference](/reference/status-reference#moscow-priority) says what each means; the sprint planner and the release composer read it. </Accordion> <Accordion title="Starts and Due"> Optional dates; the due date must be on or after the start. The roadmap places the epic from them. </Accordion> <Accordion title="Colour"> Optional. One of the configured badge colours; the reference badge, the board and the roadmap paint the epic in it. </Accordion> <Accordion title="Status, reference, number, version and approved"> Read-only in the editor. The status moves from the list or the flyout, and through the [approval bar](/planning/approvals) for the review states; the reference and the number are assigned when the epic is created; the version counts the approvals that carried and stays at zero until the first; approved is stamped by the approval, with the version that was read. </Accordion> </Accordions> ## What happens next - Every save, move and restore writes a [version](/planning/revisions-and-versions) and an entry in the project's [activity history](/workspace/activity-log). - `epic.created`, `epic.updated`, `epic.status_changed`, `epic.restored` and `epic.deleted` go out to the webhook endpoints subscribed to the `epic.*` family. - Stories, tasks, bugs and spikes hang under epics on the [Backlog](/planning/backlog) page, which groups and filters by epic; releases will be composed from them and the roadmap will place them by their dates, as those modules land. ## Heimdall on this page Heimdall reads epics and their versions through the same tools the MCP server offers and carries the open project as its context. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “Which epics are still drafts, and who owns them?” · “What is the goal of CRM-12?” · “How did the loyalty tiers epic change since last week?” Writing epics stays with people for now; the brief import will draft them into a review queue, and Heimdall will propose improvements to the ones that lack a goal, a measure or an owner. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------- | ----------------------------------------------------------------------- | | See the epics | `epic:view-any`, `epic:view`; a client guest sees the agreed epics only | | Add an epic | `epic:create` | | Edit or move an epic | `epic:update` | | Delete an epic | `epic:delete` | | Read or restore its versions | `epic:view` to read, `epic:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="Why does a guest not see my draft?"> A draft is the analyst's working note. A client guest sees an epic once it is approved and from then on, so what the client reads is always what was agreed. </Accordion> <Accordion title="Can I change the reference?"> No. The reference is spelled from the project's key and the number the epic took, and the key locks the moment the first artefact is numbered, so every reference stays true. </Accordion> <Accordion title="What happens to the stories under a deleted epic?"> They stay in the backlog with their own references and lose their epic. Cancel the epic instead when the client dropped the work: cancelling keeps the record and leaves the stories attached. </Accordion> </Accordions> ## Related - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [Backlog](/planning/backlog) — the stories, tasks, bugs and spikes under the epics - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [The planning record](/planning) — what hangs off epics as the modules land - [Status reference](/reference/status-reference#epic-status) — every epic status, priority and revision reason - [`epic.*` webhook events](/webhooks/v1/events/epic) --- # Functional Requirements Source: https://alm-docs.labs.zuselab.dev/planning/functional-requirements By the end of this page you will have written down what a project's system shall do as requirements the client can sign off, know every field a requirement carries and what reads it downstream, and know how a requirement moves from a draft to a baseline, how it is deleted, and where every version of it is kept. A **requirement** is one sentence the client and the team agree the system must honour. A **functional** requirement says what it does for a person: _the system shall let a member reset their password from the sign-in page by email_. It is written in the client's words, in the shall form, one behaviour per requirement, with where it came from, how important it is and how it will be shown to be met. Functional requirements number on their own sequence (`CRM-FR-12`), so a requirement and a story never share a key, and every save of one is kept as a [version](/planning/revisions-and-versions). The qualities the system keeps while it does all this are [non-functional requirements](/planning/non-functional-requirements), written on the same page with a category and a measure. ## Before you begin | You want to | You need | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Open the Requirements page | `requirement:view-any` on the project, which every default role carries. A client guest sees the agreed requirements only. | | Add a requirement | `requirement:create`. Project managers and business analysts have it by default. | | Edit or move a requirement | `requirement:update`. | | Delete a requirement | `requirement:delete`. | | Name an owner | The person to be a member of the team. | | Name a source stakeholder | The stakeholder to be on the project's [stakeholder list](/projects/stakeholders). | The page lives under a project: open the project and choose **Requirements** in the sidebar, badged with how many requirements you may see. ## Read the requirements [step] The table lists one requirement per row, functional and non-functional together: | Column | What it shows | | --------------- | ------------------------------------------------------------------------------------------------------ | | **Reference** | The requirement's key, painted in the colour of its kind. | | **Requirement** | The title, with the first line of the statement beneath it. Press the title to open the detail flyout. | | **Kind** | Functional or non-functional, with the category beside it for a non-functional one. | | **Status** | Draft, in review, approved, baselined or deprecated, as a badge. Hidden from a guest. | | **Priority** | The MoSCoW letter: M, S, C or W. | | **Owner** | The person who answers for the requirement, or _Unassigned_. | | **Verified By** | Test, inspection, analysis or demonstration. | | **Updated** | How long ago the record last changed. | | Row menu | **View Requirement**, **Edit Requirement**, **Move to**, **Activity**, **Delete Requirement**. | Sort by reference, title, kind, status, priority, verification method or updated by pressing a column heading; press it again to flip the direction. The list opens in the project's own order. Above the table, narrow the list with the search box (words in the reference, the title or the source), the **kind**, **category**, **status** and **priority** lists. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Add a requirement [step] 1. Press **New Requirement**. 2. Pick the **Kind** first. It decides the sequence the requirement is numbered from and cannot change later; leave it on _functional_ for a behaviour. 3. Type the **Title** and the **Statement**: what the requirement is called and, in one testable sentence in the shall form, what the system does. These two are enough; Heimdall will draft the rest from the brief once the brief import lands. 4. Write the **Rationale**, and under _Where it came from_ pick the **Stakeholder** who asked for it and type the **Source**: the meeting, document or regulation. 5. Pick the **Owner**, the **Priority** and how it is **Verified By**. 6. Press **Create Requirement**. The requirement takes the next number of its kind (the first functional requirement of `CRM` is `CRM-FR-1`), starts as a draft, appears in the table at once, writes its first version, and `requirement.created` goes out to every endpoint subscribed to it. <Callout type="info" title="One behaviour per requirement"> A requirement that says two things is tested as two things and argued about as two things. Split _the system shall send a receipt and let the member download it_ into two requirements with their own references, so each can be approved, traced and verified on its own. </Callout> ## Open a requirement [step] Press a requirement's title, or **View Requirement** in its row menu, to open the detail flyout: the reference, kind, category, status and priority at the top, then the statement and the rationale, the measure of a non-functional requirement, the stakeholder and the source, the owner, the verification method, the dates, and the **Versions** section with every save of the requirement. From the flyout: - **Edit** opens the editor on this requirement. - **Move to** changes the status in one step, offering only the moves the lifecycle allows. - **Restore this version** under any earlier version puts it back; see [revisions and versions](/planning/revisions-and-versions). - **Activity** opens the requirement's history: every change, who made it and where it came from. - **Delete** opens the confirmation. A link such as `/projects/<id>/requirements?show=<requirement id>` opens the page with that requirement's flyout already open; Heimdall's **View** buttons use it. ## Edit a requirement [step] 1. Press **Edit Requirement** in the row menu, or **Edit** in the flyout. 2. Change what you need. The kind is shown as a badge and cannot change; the owner must be a member of the team and the stakeholder one of the project's. 3. Press **Save Changes**. The save writes a new version of the requirement and `requirement.updated` goes out with the columns that changed, each with its old and new value. An edit that changes nothing writes nothing and announces nothing. The status does not change from the editor; it moves from the list or the flyout. ## Move a requirement along its lifecycle [step] A requirement reaches **In review** and **Approved** through its [approval](/planning/approvals) alone: **Submit for review** on the approval bar puts it in review, and the approvers' verdicts move it out, to approved when the request carries and back to where it stood when it is rejected or withdrawn. **Move to** in the row menu or the flyout offers the other moves the lifecycle allows from where the requirement stands, and is refused while a request is open: | From | To | | -------------- | --------------------- | | **Draft** | Deprecated | | **In review** | Draft, deprecated | | **Approved** | Baselined, deprecated | | **Baselined** | Deprecated | | **Deprecated** | Draft | An approval stamps when the requirement was agreed and at which version, and a client guest sees it from that moment on, through baselined, and loses it when it is deprecated. Every move writes a version with the reason _moved_ and `requirement.status_changed` goes out with the status before and after; an approval writes one with the reason _approval_. <Callout type="info" title="Baselined is the freeze's badge"> A [baseline](/planning/baselines) that holds an approved requirement moves it to baselined when the baseline is frozen and locks it: the editor, the status menu and the restore button refuse, naming the baseline, until an approved [change request](/planning/change-requests) unlocks it, which returns it to approved. The status menu still offers the move to baselined by hand, for a scope agreed outside the application; that badge carries no lock. </Callout> ## Delete a requirement [step] **Delete Requirement** in the row menu or **Delete** in the flyout opens the confirmation. Deleting removes the requirement with every version it kept and its activity history; its number is never handed out again. `requirement.deleted` goes out. Prefer **Deprecated** for a requirement the client withdrew: it keeps the record and the reasoning. ## All the fields <Accordions> <Accordion title="Kind"> Required, _functional_ by default, chosen on a create and fixed after. Functional for what the system does; non-functional for a quality it keeps, which adds the category and the measure described on the [non-functional requirements](/planning/non-functional-requirements) page. The kind picks the sequence the requirement is numbered from. </Accordion> <Accordion title="Title"> Required, 3 to 200 characters. What everyone calls the requirement, next to its reference, in the table, the traceability matrix, the test cases and Heimdall's answers. </Accordion> <Accordion title="Statement"> Optional rich text, and the heart of the record. What the system shall do, in one testable sentence in the client's words; test cases are derived from it and the client signs it off. Filtered to a safe subset when shown. </Accordion> <Accordion title="Rationale"> Optional rich text. Why the client needs it, so a change request later can judge what it would cost to drop. </Accordion> <Accordion title="Stakeholder and Source"> Optional. The stakeholder who asked for it, from the project's stakeholders (an approval policy may name their seat), and the meeting, document or regulation it came from, in words up to 300 characters. Both are shown in the flyout; the source is searchable. </Accordion> <Accordion title="Owner"> Optional. The person who answers for the requirement; a member of the team. The owner's name is shown in the table and the flyout. </Accordion> <Accordion title="Priority"> Required, _should have_ by default. The MoSCoW priority: must have, should have, could have or won't have. The [status reference](/reference/status-reference#moscow-priority) says what each means; the release composer and the readiness score read it. </Accordion> <Accordion title="Verified by"> Required, _test_ by default. How the requirement is shown to be met: test (a test case runs against it), inspection (the code, the configuration or the documents are examined), analysis (a calculation or a review of the data) or demonstration (walked through in front of the people who asked). The test module reads it to know which requirements get a test case. </Accordion> <Accordion title="Status, reference, number, version and approved"> Read-only in the editor. The status moves from the list or the flyout, and through the [approval bar](/planning/approvals) for the review states; the reference and the number are assigned when the requirement is created; the version counts the approvals that carried and stays at zero until the first; approved is stamped by the approval, with the version that was read. The flyout also reads the names behind the ids: the owner's and the stakeholder's. </Accordion> </Accordions> ## What happens next - Every save, move and restore writes a [version](/planning/revisions-and-versions) and an entry in the project's [activity history](/workspace/activity-log). - `requirement.created`, `requirement.updated`, `requirement.status_changed`, `requirement.restored` and `requirement.deleted` go out to the webhook endpoints subscribed to the `requirement.*` family. - Stories will implement requirements and test cases will verify them through links, the traceability matrix will show which requirement has no story and no test, and the baselines will freeze the agreed set, as those modules land. ## Heimdall on this page **Quality** in the flyout asks Heimdall to read the requirement for one testable statement, a rationale, a fitting verification method, conflicts with the project's business rules and duplicates among its siblings, and to score it out of 100 with the findings to fix. See [Quality review](/planning/quality-review). Heimdall reads requirements and their versions through the same tools the MCP server offers, drafts requirements as drafts a person reviews, and carries the open project as its context. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “Which functional requirements have no owner yet?” · “Draft a requirement for downloading a receipt as a PDF, must have, verified by demonstration.” · “Read CRM-FR-12 and tell me whether it says one thing or two.” The brief import will draft whole specifications into a review queue, and the quality review will flag a statement that is not testable or that says two things. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------- | -------------------------------------------------------------------------------------------- | | See the requirements | `requirement:view-any`, `requirement:view`; a client guest sees the agreed requirements only | | Add a requirement | `requirement:create` | | Edit or move a requirement | `requirement:update` | | Delete a requirement | `requirement:delete` | | Read or restore its versions | `requirement:view` to read, `requirement:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="Why can I not change a functional requirement into a non-functional one?"> The two kinds number on their own sequences, so the reference would have to change and every link to it would break. Add a new requirement of the other kind and deprecate this one. </Accordion> <Accordion title="Why does a guest not see my draft?"> A draft is the analyst's working note. A client guest sees a requirement once it is approved and from then on, so what the client reads is always what was agreed. </Accordion> <Accordion title="Where do I write the acceptance criteria of a requirement?"> On the story that implements it. A requirement says what the system shall do; a story's [acceptance criteria](/planning/acceptance-criteria) say how a tester knows it does, and the links between them land with the traceability module. </Accordion> </Accordions> ## Related - [Quality review](/planning/quality-review) — the Quality panel in the flyout: Heimdall's score and findings, and the badge on the row - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [Non-functional requirements](/planning/non-functional-requirements) — the qualities, with a category and a measure - [Epics](/planning/epics) and the [Backlog](/planning/backlog) — the scope and the stories that implement the requirements - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [Status reference](/reference/status-reference#requirement-status) — every status, kind, category and verification method - [`requirement.*` webhook events](/webhooks/v1/events/requirement) --- # Planning Source: https://alm-docs.labs.zuselab.dev/planning The planning record is what a business analyst writes down once and every later phase reads: the scope the client agreed to, broken into the work the team will build, with the rules, the words and the risks around it. It begins with **epics**, the bodies of work the client recognises, the **backlog** of stories, tasks, sub-tasks, bugs, spikes and improvements under them, each story with the **acceptance criteria** that say when it is done, the **requirements** the system must honour (what it shall do and the qualities it shall keep) and the **business rules** the business has decided, and the **use cases** that say how each persona reaches a goal with the system, step by step, the **RAID log** of what may go wrong and what the project leans on, the **decisions** that record every choice with its options and consequences, the **links** that join all of these into one record read from either side, and the **comments** that carry the conversation on each of them, the **baselines** that freeze the agreed record at its approved versions, and the **change requests** that are the one way to reopen a frozen record. Every artefact in it carries a **version** for every save, by a person or by Heimdall, and any version can be put back. | Piece | What it is | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Epic** | A body of work the client recognises, numbered from the project's tracker sequence (`CRM-12`), with its goal, value, measures, owner, MoSCoW priority, status from draft to done and dates. Releases are cut from epics and work items hang under them. | | **Work item** | One row of the tracker: a story, task, sub-task, bug, spike or improvement, in the same number series as the epics, with its status from the backlog to done, its priority, the people on it, its estimates and the fields its kind adds. | | **Acceptance criterion** | One sentence that says when a story is done, with an optional Given, When and Then and a kind (functional, negative, edge case, non-functional); QA ticks them and test cases are drafted from them. | | **Requirement** | One sentence the system must honour: a functional requirement (`CRM-FR-12`) says what it does, a non-functional one (`CRM-NFR-3`) a quality it keeps, with a category and a measure. Each carries its statement, source, MoSCoW priority, verification method, owner and status from draft to baselined. | | **Business rule** | A decision of the business the system must honour (`CRM-BR-7`): a constraint, derivation, computation, inference, action enabler or policy, in plain words with when it applies and what follows, exceptions, worked examples, authority, priority, the dates in force and the same status lifecycle as a requirement. | | **Use case** | How an actor reaches a goal with the system (`CRM-UC-4`): the persona who acts, the other actors, the goal, the trigger, what must be true before and after, how often it happens, the priority, the same status lifecycle as a glossary term, and the steps of the main flow with the alternate and exception flows that leave it. | | **RAID item** | An entry of the log (`CRM-RSK-4`): a risk scored by probability and impact with its mitigation and contingency, an assumption the plan rests on, an issue hurting now or a dependency on others, each with its area, owner, due or review date and a status from open to closed. Internal: a client guest never sees the log. | | **Decision** | The record of a choice the project made (`CRM-DEC-5`): its kind, the context, the options weighed with their pros and cons, what was decided, the consequences, who decided and when, the decision it supersedes, and a status from proposed to accepted, superseded or deprecated. Internal: a client guest never sees the decisions. | | **Attachment** | A file pinned on one artefact: a wireframe on a story, a signed specification on a requirement, a sample on a data entity. Kept on a private disk and served only through the application to a person who may see the artefact; added and removed, never versioned. | | **Comment** | A note on one artefact: a question, an answer under it (a thread is one level deep), a finding, a decision taken in the margin. Open to everyone who sees the artefact, or internal and kept from client guests. Names people with `@` to tell them; the first comment of a thread is resolved when the question is settled. | | **Follower** | A person told of every comment posted on an artefact: anyone who commented on it, and anyone who pressed **Follow** in its discussion panel. | | **Label** | A word the project pins on its work items (`payments`, `tech-debt`), with a colour and a meaning; the project's own vocabulary, unique within it, filtered by on the backlog and shaped from the backlog's label manager. | | **Link** | A join between two artefacts of the project with a kind read from the source (_CRM-142 implements CRM-FR-12_) and read back from the target (_CRM-FR-12 is implemented by CRM-142_), kept once in the Links panel of both flyouts. Internal: a client guest never sees the links. | | **Reference** | The key an artefact is known by, spelled from the project's key and a number that is never reused; epics and work items share one sequence, each kind of requirement and the rules have their own. | | **Priority** | MoSCoW for epics, requirements and rules (must, should, could, won't) and five steps from lowest to highest for work items; the sprint planner and the release composer read them. | | **Version** | What a save of an artefact leaves behind: the snapshot of its captured columns (a work item's criteria and a rule's examples inside it), the diff from the version before, who wrote it, through what, and why. Restoring one writes a new version rather than rewriting the past. | | **Baseline** | A named set of approved records frozen together (the scope the client signs, what a release ships, what a sprint commits to, what a milestone promised): each record pinned at its approved version and locked once the project manager's approval freezes the baseline; a scope baseline then signed by the client's stakeholders. | | **Change request** | The one way a frozen record changes (`CRM-CR-2`): why, who asked, the baseline it changes and the records it touches, the impact on scope, schedule, effort, cost and risk with a level, written by the analyst or drafted by Heimdall; its approval unlocks the records it names, and the change is seen through to verified. | <Callout type="info" title="Approved means approved"> Nobody moves an epic, a story, a requirement, a rule or a use case to approved by hand. The record is submitted, the people the project's policy names decide, and the approval stamps the version that was read; a client guest sees the record from that moment on. See [approvals](/planning/approvals). </Callout> ## Pages <Cards> </Cards> ## What reads the planning record - The [project overview](/projects/overview) and the sidebar count the epics, the open items, the requirements, the rules and the use cases a person may see, and the RAID items that still need attention. - Heimdall reads epics, the backlog, the requirements, the rules and their versions through the same tools the MCP server offers, drafts stories, requirements and rules as drafts a person reviews, and moves and assigns items when asked; the brief import will draft whole epics' worth of them into a review queue once it lands. - A [baseline](/planning/baselines) freezes approved epics, stories, requirements, rules and use cases at their approved versions and locks them; an approved [change request](/planning/change-requests) unlocks the ones it names, and nothing else does. - Use cases will link to requirements, rules and stories and share their numbering style, their versions and their review lifecycle; the board, the sprints, the test cases and the traceability matrix will read the backlog, the requirements and the rules as those modules land. ## Permissions | Action | Permission | | ----------------------------------------- | ----------------------------------------------------------------------------------------- | | See the epics | `epic:view-any` and `epic:view`; a client guest sees agreed epics | | Add, change, move an epic | `epic:create`, `epic:update` | | Delete an epic | `epic:delete` | | See the backlog | `work-item:view-any` and `work-item:view`; a client guest sees agreed items | | Add, change an item or its criteria | `work-item:create`, `work-item:update` | | Move or hand over an item | `work-item:transition`, `work-item:assign` | | Delete an item | `work-item:delete` | | See the requirements | `requirement:view-any` and `requirement:view`; a client guest sees agreed requirements | | Add, change, move a requirement | `requirement:create`, `requirement:update` | | Delete a requirement | `requirement:delete` | | See the rules | `business-rule:view-any` and `business-rule:view`; a client guest sees agreed rules | | Add, change, move a rule | `business-rule:create`, `business-rule:update` | | Delete a rule | `business-rule:delete` | | See the use cases | `use-case:view-any` and `use-case:view`; a client guest sees agreed use cases | | Add, change, move a use case | `use-case:create`, `use-case:update` | | Delete a use case | `use-case:delete` | | See the RAID log | `raid:view-any` and `raid:view`; never a client guest | | Raise, change, move an item | `raid:create`, `raid:update` | | Delete an item | `raid:delete` | | See the decisions | `decision:view-any` and `decision:view`; never a client guest | | Record, change, move a decision | `decision:create`, `decision:update` | | Delete a decision | `decision:delete` | | See the links of an artefact | `link:view-any` and `link:view`; never a client guest | | Link two artefacts | `link:create` | | Remove a link | `link:delete` | | See the discussion | `comment:view-any` and `comment:view`; a client guest the open comments only | | Post a comment | `comment:create`; a client guest only when the project's **Guests may comment** is on | | Post an internal comment | `comment:internal`; never a client guest | | Edit, resolve or delete anyone's comment | `comment:update`, `comment:delete` | | See the labels and filter by one | `work-item:view-any` | | Pin labels, add, rename or remove a word | `work-item:update` | | See and open the files on an artefact | `attachment:view-any` and `attachment:view`; a client guest on the agreed record | | Attach a file | `attachment:create`; never a client guest | | Remove anyone's file | `attachment:delete` | | Read and restore versions | The artefact's view and update permissions; staff only | | See the baselines | `baseline:view-any` and `baseline:view`; a client guest sees the frozen baselines | | Compose, change, supersede a baseline | `baseline:create`, `baseline:update` | | Submit a baseline, ask the client to sign | `baseline:freeze` | | Delete a draft baseline | `baseline:delete` | | See the change requests | `change-request:view-any` and `change-request:view`; a client guest sees their own | | Raise, change, move a change request | `change-request:create` (a client guest too, without the impact), `change-request:update` | | Analyse, submit, verify a change request | `change-request:analyse`, `change-request:approve`, `change-request:verify` | | Delete a change request never agreed | `change-request:delete` | ## Related - [Brief import](/planning/brief-import) — Heimdall drafts the whole planning record from a brief; you accept what fits - [Projects](/projects) — the record the planning pages hang under - [Status reference](/reference/status-reference) — every status, kind, priority, resolution and revision reason - [Epics API](/api/v1/reference/epics), [Work Items API](/api/v1/reference/work-items), [Requirements API](/api/v1/reference/requirements), [Business Rules API](/api/v1/reference/business-rules), [Use Cases API](/api/v1/reference/use-cases), [RAID Log API](/api/v1/reference/raid-log), [Decisions API](/api/v1/reference/decisions), [Links API](/api/v1/reference/links), [Comments API](/api/v1/reference/comments), [Watchers API](/api/v1/reference/watchers), [Labels API](/api/v1/reference/labels), [Attachments API](/api/v1/reference/attachments), [Baselines API](/api/v1/reference/baselines), [Change Requests API](/api/v1/reference/change-requests), the [Planning tools](/mcp/v1/tools/planning) and the [Build tools](/mcp/v1/tools/build) --- # Labels Source: https://alm-docs.labs.zuselab.dev/planning/labels By the end of this page you will have given a project a short vocabulary of its own, pinned those words on the stories and bugs they describe, narrowed the backlog to one word, and renamed or retired a word without touching the items that carried it. A **label** is a word a project pins on its work items: `payments`, `tech-debt`, `client-asked`. The vocabulary belongs to the project, so two projects may use the same word for different things and neither sees the other's list. Each word is unique within its project whatever its case, carries a **colour** so the backlog reads at a glance, and may carry a **meaning** in one line so two people pin it the same way. Labels are pinned from the item's editor and shown as chips on the backlog's rows and in the item's flyout; the backlog filters by one. A label is not an artefact: it carries no versions, and removing it unpins it everywhere without changing the items. ## Before you begin | You need | Where it comes from | | ------------------ | ------------------------------------------------------------------------------------------- | | A project | The [registry](/projects/registry) or the [intake wizard](/projects/intake-wizard). | | `work-item:view-any` | To see the labels and filter by them; every role has it, a client guest included. | | `work-item:update` | To pin labels on an item and to add, rename or remove words; the roles that edit the backlog have it. | ## Pin labels on an item [#pin] 1. Open the [backlog](/planning/backlog) and open an item for editing, or press **New Item**. 2. Under **Labels**, pick the words that fit. The list is searchable; a word that does not exist yet is added from **Manage Labels** beside the heading, without leaving the editor. 3. Save the item. The chips appear under the item's title on the backlog and under **Labels** in its flyout, in the label's colour. Up to twenty labels fit on one item. ## Filter by label [#filter] Pick a word in the **Any label** filter above the backlog. The list narrows to the items carrying it, the choice is kept in the page's address so a link sent to a colleague opens the same view, and clearing the filter brings every item back. The filter appears once the project has at least one label. ## Manage the vocabulary [#manage] Press **Manage Labels** beside the **Labels** heading in an item's editor. The manager lists the project's words by name with how many items carry each, and offers three things to anyone who may edit the backlog: - **Add Label** at the foot: a **Word**, a **Colour** from the palette and a **Meaning**; the word must be free within the project whatever its case. The new word is in every picker at once. - **Edit** on a row rewrites the word, the colour or the meaning in place; every item carrying the label shows the new values at once, because an item points at the label rather than copying it. - **Remove** on a row asks once, inline, then takes the word out of the vocabulary and off every item; the items are untouched. **Keep** backs out. Viewers see the list and the counts without the form or the row buttons. ## All the fields <Accordions> <Accordion title="Labels (on an item)"> The project's words pinned on the item, picked from a searchable list; up to twenty, each one of the project's own labels. Optional. Left empty, the item carries no labels; on a rewrite over the API, leaving the field out keeps the labels as they are. </Accordion> <Accordion title="Word"> One to sixty characters, a single line, unique within the project whatever its case: `Payments` and `payments` are one label. Keep the vocabulary short; a backlog with forty words filters by none of them. </Accordion> <Accordion title="Colour"> One of eighteen badge colours, zinc by default. The chip takes it wherever the label shows: the backlog row, the item's flyout, the manager. </Accordion> <Accordion title="Meaning"> Up to 160 characters saying when to pin the word, so two people pin it the same way. Optional; shown in the manager beside the word. </Accordion> </Accordions> ## What happens next - `label.created`, `label.updated` and `label.deleted` go out with the word and the colour; see the [`label.*` events](/webhooks/v1/events/label). Pinning or unpinning a label on an item announces nothing of its own: an item's labels travel with the item and are read back from the [work items API](/api/v1/reference/work-items) as `labels`. - A label is not part of an item's [versions](/planning/revisions-and-versions): restoring an earlier version of a story leaves its labels as they are, like its components. - Heimdall and an agent connected over MCP read the vocabulary with [`list_labels`](/mcp/v1/tools/build/list-labels) before they draft or filter, and name labels by id when they list work items with `label_id`. ## Managing - **Rename rather than replace**: editing a word keeps it on every item; removing it and adding a new one starts from zero. - **A removed label** disappears from every item and from the filter at once; the items keep their other labels and announce nothing. - **A deleted project** takes its vocabulary with it. - **Client guests** see the chips on the agreed items and may filter by them; they never shape the vocabulary. ## Heimdall on this page Heimdall reads a project's labels as part of its vocabulary and uses the words when it drafts or summarises the backlog; an agent connected over MCP reads them with `list_labels` and pins them through the stories it drafts. Neither coins, renames or removes a word: the vocabulary is the team's, kept short on purpose. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ------------------------------------ | ----------------------------------------------------------------- | | See the labels and filter by one | `work-item:view-any` | | Pin labels on an item | `work-item:update`, or `work-item:create` on a new item | | Add, rename or remove a word | `work-item:update` | ## Frequently asked <Accordions> <Accordion title="Why can I not add a word that is already there in another case?"> A word is unique within the project whatever its case, so `Payments` and `payments` would be the same label twice. Pick the existing word, or rename it in the manager if its case is wrong. </Accordion> <Accordion title="Can I label an epic or a requirement?"> Not yet. Labels are pinned on work items today; the table behind them is built to take other artefacts, and they join as their modules need them. </Accordion> <Accordion title="Where is the filter?"> Above the backlog, beside the epic and assignee filters, once the project has at least one label. A project with no labels shows no filter, because there is nothing to pick. </Accordion> </Accordions> ## Related - [Backlog](/planning/backlog) — the items the words are pinned on - [Components](/projects/components) — the other way items are grouped, by the part of the product they change - [Traceability](/planning/traceability) — relations between artefacts, as opposed to words on them - [`label.*` webhook events](/webhooks/v1/events/label) --- # Non-Functional Requirements Source: https://alm-docs.labs.zuselab.dev/planning/non-functional-requirements By the end of this page you will have written the qualities a project's system shall keep as non-functional requirements with a category and a measurable target, know which category each quality belongs to and what reads it, and know how a target is written so it can be tested. A **non-functional requirement** says a quality the system keeps while it does what the functional requirements say: how fast, how available, how safe, how accessible. It is written on the same [Requirements page](/planning/functional-requirements) with the same fields, plus two of its own: the **category** the quality belongs to and the **measure**, the metric and the target that say when the requirement is met. Non-functional requirements number on their own sequence (`CRM-NFR-3`), and the house stack brings presets for most categories: a Laravel and Livewire application has well-known answers for performance, security, accessibility and availability, and the standards step of the intake wizard will offer them once it lands. ## Before you begin | You want to | You need | | ------------------------------------ | -------------------------------------------------------------------------------- | | Read the non-functional requirements | `requirement:view-any` on the project. A client guest sees the agreed ones only. | | Add or change one | `requirement:create` to add, `requirement:update` to change. | Everything else on the page (opening, moving, deleting, the versions and the permissions) is as the [functional requirements](/planning/functional-requirements) page describes. ## Write a non-functional requirement [step] 1. Press **New Requirement** on the Requirements page and set the **Kind** to _non-functional_. The _Quality and Measure_ fieldset appears. 2. Pick the **Category**: the quality the requirement is about. The list below says what each covers. 3. Type the **Title** and the **Statement** as for any requirement: one sentence in the shall form, in the client's words. 4. Type the **Measure**: the metric, the target and where it is measured, such as _p95 under 300 ms at 500 concurrent users, measured on staging_. 5. Fill the source, the owner, the priority and the verification method, and press **Create Requirement**. The requirement takes the next number of the non-functional sequence (`CRM-NFR-1` for the first), starts as a draft and writes its first version. A non-functional requirement without a category is refused; a category on a functional requirement is ignored. <Callout type="info" title="A quality without a number cannot be tested"> _The system shall be fast_ is an opinion. _The system shall render the tier page within 300 ms at the 95th percentile with 500 members browsing at once_ is a requirement a test can pass or fail. Put the number in the measure, say where it is measured, and pick the verification method that can produce it. </Callout> ## Pick the category [step] | Category | What it covers | A typical measure | | ------------------- | --------------------------------------------------------------- | -------------------------------------------------------------- | | **Performance** | How fast it answers and how much it handles under load. | p95 response time at a stated concurrency. | | **Scalability** | How it grows with more users, data or traffic. | Users or rows it serves without a change of architecture. | | **Availability** | How much of the time it is up, and when it may be down. | Monthly uptime, and the maintenance window. | | **Reliability** | How rarely it fails and how it recovers when it does. | Error rate, recovery time, data loss allowed. | | **Security** | How it keeps out who should be out and keeps data safe. | A standard met, a control in place, a test passed. | | **Privacy** | How personal data is collected, kept and removed. | Retention period, erasure time, consent recorded. | | **Usability** | How easily a person gets things done with it. | Task completion time or rate in a usability test. | | **Accessibility** | How people with disabilities use it, and the standard it meets. | A WCAG level with zero failures in an audit. | | **Compatibility** | The browsers, devices, systems and versions it works with. | The named browsers and versions it is tested on. | | **Maintainability** | How easily the team changes it without breaking it. | Test coverage, build time, a quality gate kept green. | | **Portability** | How it moves between environments, hosts or providers. | A deploy to a fresh host in a stated time. | | **Compliance** | The laws, regulations and standards it must satisfy. | The regulation named and the evidence it is met. | | **Observability** | How its health, errors and behaviour can be seen from outside. | Health checks, error reporting and logs retained for a period. | | **Localisation** | The languages, formats, currencies and regions it serves. | The languages and formats it ships with. | | **Other** | A quality none of the above names. | Whatever makes it testable. | The [status reference](/reference/status-reference#nfr-category) lists the categories with their codes. The readiness score will read which categories have a measure and which have none, and the standards presets will propose one requirement per category the house stack is held to. ## Read the measure [step] The table shows the category beside the kind, and the detail flyout shows the **Measure** under the statement; a non-functional requirement without a measure reads _No target set yet_. Filter the page by **category** to see every requirement of one quality, or by **kind** to see the non-functional ones together. ## All the fields <Accordions> <Accordion title="Category"> Required for a non-functional requirement, absent for a functional one. One of the fifteen qualities in the table above; the editor describes the chosen one under the field. Shown as a badge in the table and the flyout, filterable, and read by the standards presets and the readiness score. </Accordion> <Accordion title="Measure"> Optional, up to 500 characters, kept for a non-functional requirement only. The metric, the target and where it is measured, in one line. Shown in the flyout and sent as `measure` over the API; the test module will derive the check from it. </Accordion> </Accordions> ## What happens next - The category and the measure travel in every [version](/planning/revisions-and-versions) of the requirement and in the `changes` of `requirement.updated`. - The standards step of the intake wizard, the quality review and the readiness score will read the categories and the measures as those modules land. ## Heimdall on this page **Quality** in the flyout has Heimdall check that the measure names a number, a unit and a condition and that the verification method fits, beside everything it checks on any requirement, and score the requirement out of 100. See [Quality review](/planning/quality-review). Heimdall reads the non-functional requirements through the same tools the MCP server offers and drafts them with a category and a measure when asked. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “Which non-functional requirements have no measure yet?” · “Draft the performance, availability and accessibility requirements a Laravel and Livewire portal is usually held to.” · “Is the security requirement's measure something a test can check?” ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ----------------------------------- | ------------------------------------------ | | See the non-functional requirements | `requirement:view-any`, `requirement:view` | | Add or change one | `requirement:create`, `requirement:update` | ## Frequently asked <Accordions> <Accordion title="Can one requirement belong to two categories?"> No. Pick the one the measure belongs to and write a second requirement for the other quality, so each has one target a test can check. </Accordion> <Accordion title="What if the client has no number yet?"> Write the requirement without a measure, set its priority, and leave it as a draft; the flyout reads _No target set yet_ and the quality review will keep asking for one until it is written. </Accordion> </Accordions> ## Related - [Quality review](/planning/quality-review) — the Quality panel in the flyout: Heimdall's score and findings, and the badge on the row - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [Functional requirements](/planning/functional-requirements) — the page, the fields, the lifecycle and the versions - [Status reference](/reference/status-reference#nfr-category) — the categories and the verification methods - [`requirement.*` webhook events](/webhooks/v1/events/requirement) --- # Quality review Source: https://alm-docs.labs.zuselab.dev/planning/quality-review By the end of this page you will have asked Heimdall to review a story, read its score and the findings behind it, taken one finding to the Heimdall panel to fix it, reviewed the story again and watched the badge on the backlog follow; you will also know exactly what Heimdall checks for each kind of record, how the score is built, what a review costs and what it announces. A **quality review** is Heimdall reading one planning record the way the people after you will: a developer who has to build it, a tester who has to prove it, a client who has to sign it off. It reads the record, the rows that belong to it, what it is linked to, the project's business rules and the titles of its siblings, and answers with **findings**, each a plain sentence on what is wrong or missing, quoting the words concerned, with a suggestion in the record's own words. The **score** out of 100 is arithmetic over the findings, so a 71 always means the same thing, and the **band** turns it into one word for a badge: ready, needs work or not ready. Reviews live in the **Quality** panel of the detail flyout on three pages: the [Backlog](/planning/backlog) for stories, tasks, bugs, spikes and improvements, the [Requirements](/planning/functional-requirements) page for functional and non-functional requirements, and the [Business rules](/planning/business-rules) page. Nothing on the record changes when Heimdall reviews it: the findings are yours to act on, and the record's history stays the record of what people and accepted drafts wrote. ## Before you begin | To | You need | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Read a record's reviews | A staff account that can open the record. A client guest never sees the panel. | | Ask Heimdall for a review | `ai:use` on the team and the project's **AI assists** switch on under Settings → AI; a client guest never asks. | | Take a finding to the Heimdall panel | The same `ai:use`, since the panel is Heimdall's. | | Have room in the team's monthly AI budget | A review is one metered call; when the month's cap is reached Heimdall refuses until an admin raises it. | The record must be saved first: Heimdall reviews what is on the record, not what is in an open editor, and the review remembers which version it read. ## Reviewing a record <Steps> <Step> ### Open the record and find the Quality panel Open the story, requirement or rule from its list; the flyout shows the record, then its versions, then the **Quality** panel, above the links. A record never reviewed shows one sentence on what Heimdall looks for and a **Review now** button. </Step> <Step> ### Press Review now Heimdall reads the record and what the project holds around it, which takes a few seconds; the button says **Heimdall is reading…** until the answer lands. A toast tells you the score and how many findings there are, the panel fills, and the record's row in the list gains a small score badge. </Step> <Step> ### Read the score and the summary The badge at the top of the panel shows the score out of 100 and its band, who asked and when, and the version of the record that was read. Under it, two or three sentences from Heimdall on the record as a whole: what is sound and what the findings add up to. A record with nothing to fix scores 100 and says so. </Step> <Step> ### Read the findings, most serious first Each finding is one card: a severity badge, the kind of problem (hover it for one line on what the kind means), the field concerned when there is one, the finding itself quoting the words Heimdall objects to, and **Try:** followed by a suggestion in the record's own words. Findings are ordered by severity, high first. </Step> <Step> ### Take a finding to Heimdall **Ask Heimdall** on a finding opens the Heimdall panel with the finding already written into the composer, naming the record and quoting the message and the suggestion. Nothing is sent until you press Enter, so add what Heimdall could not know, then let it read the record with its tools and propose the rewrite; you make the change in the editor yourself. </Step> <Step> ### Fix the record and review again Edit the record as usual and save. The panel now shows **Record changed since** beside the score, because the review read an earlier version. **Review again** asks Heimdall for a fresh reading; the earlier reviews stay on the record, newest first, so the history of a story's quality reads in order over the API. </Step> </Steps> <Callout type="info"> Every review is one call to the model, filed in the team's AI ledger under **Quality reviews** and counted against the month's budget on [Settings → AI](/workspace/ai-settings). Review a record when it changed or was never reviewed, not after every keystroke; the panel always shows the latest review, so there is nothing to gain from asking twice. </Callout> ## How the score is built Heimdall never names a number. It names findings and grades each one, and the score is 100 less the penalties of the findings, never below 0: | Severity | Meaning | Penalty | | ---------- | ----------------------------------------------------------------- | ------- | | **High** | A developer or a tester would be blocked or misled by the record. | 25 | | **Medium** | The record is usable but would be argued over. | 10 | | **Low** | Polish: a word, a missing example, a sharper title. | 4 | The band reads the score for you: | Band | Score | Read it as | | -------------- | --------- | ---------------------------------------------------------------------------------- | | **Ready** | 80 to 100 | Fit for the people after you; the findings, if any, are polish. | | **Needs work** | 50 to 79 | Usable with questions; fix the medium and high findings before it is picked up. | | **Not ready** | 0 to 49 | Something would block a developer or a tester; rewrite before anyone estimates it. | Two high findings alone take a record to 50, so a story missing its criteria and hiding three stories in one title is not ready whatever else it does well. That is the point: the number reflects what would hurt downstream, not how long the record is. ## What Heimdall checks <Accordions> <Accordion title="A story"> The **INVEST** qualities, one finding per breach: **Independent** (the story makes sense without another story), **Negotiable** (it describes the outcome, not the implementation), **Valuable** (the "so that" names what a persona or the business gets), **Estimable** (clear enough to size), **Small** (one sprint's worth; an epic in disguise is split), **Testable** (each acceptance criterion can pass or fail). Then ambiguous words (fast, easy, user-friendly, appropriate, as needed, some), criteria missing or restating the title, criteria that cannot be verified, a criterion contradicting a business rule of the project, and a sibling story that may already be the same story. Heimdall reads the story sentence, the description, every acceptance criterion with its Given, When and Then, the labels, the epic, the persona, the links, the project's business rules and the titles of the other stories. </Accordion> <Accordion title="A task, sub-task, bug, spike or improvement"> A clear outcome and a way to tell the work is done. For a bug, the steps to reproduce and the expected versus actual result; for a spike, the question and the timebox. Vague words and duplicates are named as for a story. </Accordion> <Accordion title="A requirement"> One testable statement in "the system shall" form, no ambiguity, a rationale, a measure with a target for a non-functional requirement (a number, a unit, a condition), a verification method that fits, no conflict with a business rule and no duplicate of another requirement of the same kind. Heimdall reads the statement, the rationale, the measure, the verification method, the source, the links, the project's rules and the titles of the other requirements. </Accordion> <Accordion title="A business rule"> One rule, condition and action both stated, exceptions listed when they exist, at least one worked example, an authority named, no conflict or overlap with another rule of the project, and plain language the client can read. Heimdall reads the statement, the condition, the action, the exceptions, the examples, the authority, the dates in force, the links and every other rule of the project in full, since a rule's twin hides in another rule's statement. </Accordion> <Accordion title="Every record"> A record that assumes a technology the house does not build with is flagged as off the house stack, so a story written for another framework is caught before a developer reads it. </Accordion> </Accordions> ### The kinds of finding | Finding | Code | Meaning | | ------------------------- | ------------------- | --------------------------------------------------------------------- | | **Not independent** | `independent` | The story only makes sense together with another story. | | **Over-specified** | `negotiable` | The story dictates the implementation instead of the outcome. | | **Value unclear** | `valuable` | The story does not say what a persona or the business gets from it. | | **Hard to estimate** | `estimable` | The story is too vague to size. | | **Too large** | `small` | The story is an epic in disguise and should be split. | | **Not testable** | `testable` | No criterion of the story can pass or fail. | | **Ambiguous wording** | `ambiguous` | Words such as fast, easy, appropriate or some leave the meaning open. | | **Criteria missing** | `missing_criteria` | The story has no acceptance criteria, or they restate the title. | | **Cannot be verified** | `untestable` | A statement or criterion cannot be verified. | | **No measure** | `missing_measure` | A non-functional requirement has no measurable target. | | **No rationale** | `missing_rationale` | Nothing says why the requirement exists. | | **No worked example** | `missing_examples` | The rule has no worked example. | | **Incomplete** | `incomplete` | A condition, action, expected result or outcome is missing. | | **Conflicts with a rule** | `rule_conflict` | The record contradicts a business rule of the project, named. | | **Possible duplicate** | `duplicate` | Another record of the project may already say the same, named. | | **Off the house stack** | `stack_drift` | The record assumes a technology the house does not build with. | | **Other** | `other` | Something else worth fixing. | ## Convening the council Beside **Review now**, people who may use Heimdall see **Convene the council**: a menu that summons every voice the project seats on a requirement review at once (**Every seated voice**) or one of them alone (**Ask Thor**, **Ask Loki**, and so on), for the question you have. Each voice reads the record through its own lens, Heimdall writes the verdict, and the Heimdall panel opens on its **Council** tab to show the sitting as it fills in; a line under the review names the newest sitting's state with **Open the sitting**. One sitting per record at a time; the menu returns when it is done. Who sits, what each voice reads, what accepting a finding writes and what a sitting costs is on the [council](/copilot/council) page. ## The badge on the list Once a record was reviewed, its row on the Backlog, the Requirements page and the Business rules page shows a small badge beside the reference with the latest score, coloured by its band; hover it for the band's name. The badge is the latest review whatever the record did since, so a story rewritten after a poor review keeps its old score until someone reviews it again. The three pages refresh the badge the moment a review finishes in the flyout. ## What happens next - The review is kept on the record with its findings, the version it read, who asked and the ledger row that filed the call; earlier reviews are never overwritten. - The call is filed in the team's AI ledger under **Quality reviews** and moves the month's budget; crossing the warning line or the cap tells the team's admins the way every other call does. - `work_item.quality_reviewed`, `requirement.quality_reviewed` or `business_rule.quality_reviewed` goes out with the score, the number of findings and the version read, so a dashboard can follow a project's quality without reading the findings. - The record itself is untouched: no version is written, no status moves, no field changes. - When Heimdall cannot reach its model, the panel says so, nothing is filed and nothing is kept; try again in a moment. ## From the API and MCP <Cards> </Cards> An agent reads a record's reviews with `activity:read` and asks for one with `ai:use`; the reads need the record's own view ability over REST. A client guest's token is refused both ways. ## Permissions | Action | Permission | | ------------------------------ | ------------------------------------------------------------------------ | | See the Quality panel | A staff account that can open the record; never a client guest | | Ask for a review | `ai:use`, with the project's AI assists on | | Read reviews over REST | `work-item:view`, `requirement:view` or `business-rule:view`, staff only | | Ask for a review over REST | `ai:use` | | Read reviews from an agent | `activity:read` | | Ask for a review from an agent | `ai:use` | ## Frequently asked **Does a review change my record?** No. It is kept beside the record and shown in the panel; you decide what to do with each finding, and the record's history records only what you and accepted drafts wrote. **Why did the score not change after I fixed a finding?** The panel shows the latest review, which read the version before your edit; it says **Record changed since** to tell you. Press **Review again** for a fresh reading. **Can I review a record that is approved or baselined?** Yes. A review reads; it never writes, so the approval rules do not apply. The findings are still useful for the change request that follows. **Why is the panel missing for my client?** A client guest never sees reviews: findings are the team's working notes and often quote the gaps the client has not seen yet. **How much does a review cost?** One model call, filed under **Quality reviews** on [Settings → AI](/workspace/ai-settings) with the tokens it used. A long story with many criteria and a project with many rules costs more than a short one, because Heimdall reads them all. **Can Heimdall review on every save?** Not today: a review is asked for, so the budget is spent on purpose. The council that lands with the brief import will review a record when it is submitted for approval. ## Related - [Backlog](/planning/backlog) — the stories, tasks and bugs the panel sits on - [Acceptance criteria](/planning/acceptance-criteria) — the rows a story is judged by - [Functional requirements](/planning/functional-requirements) and [Non-functional requirements](/planning/non-functional-requirements) — statements, measures and verification methods - [Business rules](/planning/business-rules) — the rules a conflict is checked against - [Revisions and versions](/planning/revisions-and-versions) — the version a review read, and why the record's history stays untouched - [Inline assists](/copilot/inline-assist) — rewrite a field once you know what is wrong with it - [Heimdall and quality](/copilot/quality-review) — how the reviewer thinks and what it costs - [Settings → AI](/workspace/ai-settings) — the budget the reviews draw on - [Status reference](/reference/status-reference#quality-band) — bands and severities --- # RAID Log Source: https://alm-docs.labs.zuselab.dev/planning/raid-log By the end of this page you will have written down what may go wrong on a project and what it leans on, as a log the review meeting walks every week; know every field a RAID item carries, how a risk is scored and placed on the matrix, and what reads the log downstream; and know how an item moves from raised to closed, how it is deleted, and where every version of it is kept. The **RAID log** holds four kinds of entry on one sequence (`CRM-RSK-4`): a **risk** is something that may happen and would hurt the project, scored by how likely and how bad, with a mitigation (what the team does now) and a contingency (what happens if it occurs); an **assumption** is something taken as true for the plan to hold, reviewed until it is confirmed or fails; an **issue** is something that has happened and hurts now, scored by how bad; a **dependency** is something the project needs from outside itself. They share a log and a sequence because the same meeting walks all of them and an assumption that fails becomes an issue without changing its key. The log is internal: a client guest never sees it. Every save of an item is kept as a [version](/planning/revisions-and-versions). ## Before you begin | You want to | You need | | --------------------- | ------------------------------------------------------------------------------------------------------------ | | Open the RAID Log | `raid:view-any` on the project, which every staff role carries. A client guest is refused at the door. | | Raise an item | `raid:create`. Project managers, business analysts, developers and QA engineers have it by default. | | Edit or move an item | `raid:update`. | | Delete an item | `raid:delete`. Project managers and business analysts have it by default. | | Name an owner | The person to be a member of the team. | The page lives under a project: open the project and choose **RAID Log** in the sidebar, badged with how many items still need attention (open, monitoring, mitigating or occurred). The page is the first tab of its family; the second, **Decisions**, holds the [decision records](/planning/decisions). ## Read the log [step] The table lists one item per row: | Column | What it shows | | ------------- | --------------------------------------------------------------------------------------------------------------- | | **Reference** | The item's key, painted in the colour of its kind. | | **Item** | The title, with the area and the first line of the description beneath it. Press the title to open the flyout. | | **Kind** | Risk, assumption, issue or dependency, with the kind's icon. | | **Status** | Open, monitoring, mitigating, resolved, closed, occurred or accepted, as a badge. | | **Score** | Probability times impact for a risk, the impact for an issue; red from fifteen, amber from eight, green below. | | **Owner** | The person who answers for the item, or _Unassigned_. | | **Next Date** | _Due_ for an issue or a dependency, _Review_ for a risk or an assumption, or _No date_. | | **Updated** | How long ago the record last changed. | | Row menu | **View Item**, **Edit Item**, **Move to**, **Activity**, **Delete Item**. | Sort by reference, title, kind, status, score, the due date or updated by pressing a column heading; press it again to flip the direction. The list opens in the project's own order. Above the table, narrow the list with the search box (words in the reference, the title or what a dependency depends on), the **kind**, **status** and **area** lists and the **owner** list. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Read the risk matrix [step] While the project has an open risk with a probability and an impact, a five by five matrix sits above the table: probability runs left to right from _rare_ to _almost certain_, impact bottom to top from _negligible_ to _severe_, and each cell counts the open risks that sit there. A cell is green while probability times impact is under eight, amber from eight, red from fifteen; the further up and to the right a risk sits, the sooner it needs a mitigation. Resolved, closed and accepted risks leave the matrix; a risk that occurred stays on it until it is closed. ## Raise an item [step] 1. Press **Raise Item**. 2. Pick the **Kind** and the **Area**. A risk is the default; the kind decides which fields the editor shows. 3. Type the **Title**: what may happen, or what has happened, not what to do about it. This alone is enough; Heimdall will propose the rest. 4. Write the **Description**. 5. For a risk, rate the **Probability** and the **Impact** from one to five and write the **Mitigation** and the **Contingency**; for an issue, rate the impact. 6. For a dependency, say what it **Depends On**. 7. Pick the **Owner** and set the **Due On** date (issues and dependencies) or the **Review On** date (risks and assumptions), and press **Raise Item**. The item takes the next number of the log's sequence whatever its kind (the first item of `CRM` is `CRM-RSK-1`), records who raised it, starts open, appears in the table at once (a risk on the matrix too), writes its first version, and `raid_item.created` goes out to every endpoint subscribed to it. <Callout type="info" title="A risk without a score is a worry, not a risk"> Rate how likely and how bad before you leave the editor. The score places the risk on the matrix, orders the log and decides what the review meeting looks at first; a risk without one sits at the bottom and is forgotten. </Callout> ## Open an item [step] Press an item's title, or **View Item** in its row menu, to open the detail flyout: the reference, kind, status and area at the top, the title, then the description, the probability, impact and score for a risk or an issue, the mitigation and the contingency for a risk, the owner and who raised it, what a dependency depends on, the due or the review date, the dates, and the **Versions** section with every save of the item. From the flyout: - **Edit** opens the editor on this item. - **Move to** changes the status in one step, offering only the moves the lifecycle allows. - **Restore this version** under any earlier version puts it back, the score derived again; see [revisions and versions](/planning/revisions-and-versions). - **Activity** opens the item's history: every change, who made it and where it came from. - **Delete** opens the confirmation. A link such as `/projects/<id>/raid?show=<item id>` opens the page with that item's flyout already open; Heimdall's **View** buttons use it. ## Edit an item [step] 1. Press **Edit Item** in the row menu, or **Edit** in the flyout. 2. Change what you need. Changing the kind keeps the reference and drops the fields the new kind does not carry: an assumption keeps no probability, no impact and no mitigation. The owner must be a member of the team. 3. Press **Save Changes**. The save derives the score again, writes a new version of the item, and `raid_item.updated` goes out with the columns that changed, each with its old and new value, the score among them when it moved. An edit that changes nothing writes nothing and announces nothing. The status does not change from the editor; it moves from the list or the flyout. ## Move an item along its lifecycle [step] **Move to** in the row menu or the flyout offers the moves the lifecycle allows from where the item stands: | Status | Meaning | May move to | | -------------- | --------------------------------------------------------------------------------- | ---------------------------------- | | **Open** | Raised and not yet looked at. | Any other status | | **Monitoring** | Watched at every review; nothing to do yet. | Any other status | | **Mitigating** | Being worked on to make it less likely or less bad. | Any other status | | **Resolved** | Dealt with: the issue fixed, the dependency delivered, the assumption confirmed. | Any other status | | **Occurred** | The risk happened; the contingency is now the plan. | Any other status | | **Accepted** | Lived with on purpose; nobody will act on it. | Any other status | | **Closed** | Off the log; kept for the record. | Open | Open, monitoring, mitigating and occurred count as needing attention: they fill the sidebar badge and keep a risk on the matrix. Every move writes a version with the reason _moved_ and `raid_item.status_changed` goes out with the status before and after. ## Delete an item [step] **Delete Item** in the row menu or **Delete** in the flyout opens the confirmation. Deleting removes the item with every version it kept and its activity history; its number is never handed out again. `raid_item.deleted` goes out. Prefer **Closed** for an item that no longer matters: it keeps the record and the reasoning. ## All the fields <Accordions> <Accordion title="Kind"> Required, _risk_ by default, and free to change later because every item numbers on the same sequence. Risk, assumption, issue or dependency; the [status reference](/reference/status-reference#raid-kind) says what each is for. The kind decides which fields the editor shows and which the record keeps. </Accordion> <Accordion title="Area"> Required, _other_ by default. The part of the project the item touches: technical, schedule, scope, resource, budget, external, security, compliance, quality or other. It groups the log at the review meeting and filters the table; it never changes how an item is scored. </Accordion> <Accordion title="Title"> Required, 3 to 200 characters. What may happen or what has happened, next to the reference in the table and Heimdall's answers. Say the event, not the remedy. </Accordion> <Accordion title="Description"> Optional rich text. What the item is and why it matters, in enough words that the next review needs no reminder. Filtered to a safe subset when shown. </Accordion> <Accordion title="Probability and Impact"> Each from one to five. Probability (rare to almost certain) is kept for a risk only; impact (negligible to severe) for a risk or an issue. The score is their product for a risk and the impact alone for an issue, derived again on every save and restore, never typed; it orders the log, colours the badge and places a risk on the matrix. </Accordion> <Accordion title="Mitigation and Contingency"> Optional rich text, kept for a risk only. What the team does now to make it less likely or less bad, and what happens if it occurs anyway; when a risk occurs, the contingency becomes the plan. </Accordion> <Accordion title="Owner"> Optional. The person who answers for the item at the review meeting; a member of the team. The owner's name is shown in the table and the flyout and filters the list. </Accordion> <Accordion title="Due on and Review on"> Optional dates. An issue or a dependency carries a due date, when it must be resolved or delivered for the plan to hold; a risk or an assumption carries a review date, when the team next looks at it, or none to review it at every meeting. The table shows whichever the kind carries as the next date. </Accordion> <Accordion title="Depends on"> Optional, up to 200 characters, kept for a dependency only. Who or what the project is waiting on, in words; the links module will tie a dependency to another project or a work item. </Accordion> <Accordion title="Status, reference, number, score and raised by"> Read-only in the editor. The status moves from the list or the flyout; the reference and the number are assigned when the item is raised; the score is derived from the ratings; who raised the item is the person who pressed the button, the token's holder or the agent's user. The flyout also reads the owner's name behind the id. </Accordion> </Accordions> ## What happens next - Every save, move and restore writes a [version](/planning/revisions-and-versions) and an entry in the project's [activity history](/workspace/activity-log). - `raid_item.created`, `raid_item.updated`, `raid_item.status_changed`, `raid_item.restored` and `raid_item.deleted` go out to the webhook endpoints subscribed to the `raid_item.*` family. - The project overview will count the open items and show the top risks, the status report will list them, the sprint planner will weigh the dependencies, and a risk that occurs will become work through a link, as those modules land. ## Heimdall on this page Heimdall reads the log and its versions through the same tools the MCP server offers, raises a risk as open when a story, a plan or a conversation shows something that may go wrong, and carries the open project as its context. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “Which risks score fifteen or more and have no mitigation yet?” · “The checkout story depends on the payment provider's sandbox; raise that as a dependency with a due date.” · “Which assumptions are still unreviewed this month?” The brief import will draft the risks and assumptions a brief implies into a review queue, and the planning assistant will weigh the open dependencies before it proposes a sprint. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------- | --------------------------------------------------------------------------- | | See the log | `raid:view-any`, `raid:view`; never a client guest | | Raise an item | `raid:create` | | Edit or move an item | `raid:update` | | Delete an item | `raid:delete` | | Read or restore its versions | `raid:view` to read, `raid:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="Is this a risk or an issue?"> A risk may happen; an issue has happened. Raise a risk while it is still ahead of you, move it to _occurred_ when it lands, and raise the work the contingency calls for; raise an issue when something hurts the project today and nobody saw it coming. </Accordion> <Accordion title="Why does the client not see the log?"> The log is where the team is candid about what may go wrong and whom it waits on. A client reads the agreed scope and the status reports, which say what the team chose to say; the raw log stays internal. </Accordion> <Accordion title="Can I change an assumption into an issue?"> Yes, from the editor at any time. Every item numbers on the same sequence, so the reference stays and only the badge and the fields the kind carries change. </Accordion> </Accordions> ## Related - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [Use cases](/planning/use-cases) — the flows a dependency or a risk may block - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [Status reference](/reference/status-reference#raid-kind) — every kind, area and status - [`raid_item.*` webhook events](/webhooks/v1/events/raid-item) --- # Revisions and Versions Source: https://alm-docs.labs.zuselab.dev/planning/revisions-and-versions By the end of this page you will know what a version is and when one is written, how to read an artefact's versions and see what each changed, who changed it and through what, how to put an earlier version back, and what a client guest, a token and an agent may do with them. Every change a person or Heimdall makes to a planning artefact writes a **version**: the record as it was after the save, the columns it changed with their values before and after, who wrote it, through what (the dashboard, a token, an agent, Heimdall) and why (created, edited, moved, restored, drafted, imported). Nothing is ever rewritten: restoring an earlier version writes a new one that names the version it put back, so the history reads forwards and nothing is lost. Epics, work items, requirements, business rules, use cases, RAID items, decisions, glossary terms, data entities, personas and stakeholders carry versions today, a work item's acceptance criteria, a rule's worked examples, a use case's steps, a decision's options and a data entity's fields inside their versions; every artefact joins as it lands. ## Before you begin | You want to | You need | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Read an artefact's versions | Its view permission (`epic:view`, `work-item:view`, `requirement:view`, `business-rule:view`, `use-case:view`, `raid:view`, `decision:view`, `glossary:view`, `data-entity:view`, `persona:view`, `stakeholder:view`) on the project and a staff account. A client guest reads the agreed record, never the drafts behind it. | | Restore a version | Its update permission (`epic:update`, `work-item:update`, `requirement:update`, `business-rule:update`, `use-case:update`, `raid:update`, `decision:update`, `glossary:update`, `data-entity:update`, `persona:update`, `stakeholder:update`). | ## When a version is written [step] A version is written after every save that changes one of the artefact's **captured columns**: for an epic, the title, the prose, the owner, the priority, the status, the dates and the colour; for a work item, its kind, parent, epic, title, prose, status, resolution, priority, people, estimates, due date, the fields its kind adds and its acceptance criteria rows, so a story and its criteria are kept and restored together; for a requirement, its kind, category, title, statement, rationale, source, priority, measure, verification method, status and owner; for a business rule, its kind, name, statement, condition, action, exceptions, worked examples, authority, priority, the dates in force, status and owner; for a use case, its title, actor, other actors, goal, trigger, conditions, frequency, priority, status and its step rows, so a use case and its flows are kept and restored together; for a RAID item, its kind, title, description, area, probability, impact, mitigation, contingency, owner, dates, dependency and status, the score derived again on every restore; for a decision, its kind, title, context, the options weighed, what was decided, the consequences, status, the day decided, the deciders and the decision it supersedes; for a glossary term, the term, its definition, aliases, related terms, source and status; for a data entity, its component, name, plural, description, owner, PII flag, retention note, status and its fields; for a persona, everything but the picture and the archive flag; for a stakeholder, everything but the linked account. A save that changes none of them (a reorder, a counter) writes nothing, so the history never pads. | Reason | Written when | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Created** | The artefact is added, by a person, a token or an import. | | **Edited** | A person changes the fields by hand, in the editor or through the API. | | **Moved** | The artefact moves along its lifecycle, such as draft to in review. | | **Restored** | An earlier version is put back. | | **Drafted by Heimdall** | Heimdall drafts a change and a person accepts it: an inline draft taken into a field, or a record written through the panel, which names the thread. | | **Imported** | The change arrives from a brief import. | | **Approved** | An [approval](/planning/approvals) carried: the version the approvers read, pinned on the artefact as its approved version. | | **Baselined** | A [baseline](/planning/baselines) that holds the artefact was frozen: the version is the one pinned, and a requirement or rule moved to baselined with it. | | **Saved as a version** | Reserved for the manual-save flow as it lands. | Each version also records **through what** it was written: a person in the dashboard, an API token, an MCP client, an integration, a webhook, Heimdall, or the system. The [status reference](/reference/status-reference#revision-reason) lists every reason. ## Read the versions [step] Open an artefact's detail flyout, on the [Epics](/planning/epics) page for an epic, the [Backlog](/planning/backlog) for a work item, the [Requirements](/planning/functional-requirements) page for a requirement, the [Business Rules](/planning/business-rules) page for a rule or the [Use Cases](/planning/use-cases) page for a use case or the [RAID Log](/planning/raid-log) page for a RAID item or the [Decisions](/planning/decisions) page for a decision, the [Glossary](/projects/glossary) or [Data dictionary](/projects/data-dictionary) tab for a term or an entity, or the [Personas](/projects/personas) or [Stakeholders](/projects/stakeholders) tab for the people, and scroll to **Versions**. Newest first, each version shows: 1. Its number (`v3`), its reason as a badge, and **Current** on the latest. 2. One line on what it is: _Created_, _Edited: Goal, Priority_, _Moved to Approved_, _Restored version 1_. 3. Who wrote it and when, with the origin beside the name when it was not a person in the dashboard. 4. The columns it changed, each with the value before (struck through) and after. **Show more versions** reveals the next five. The **Activity** button beside it opens the [activity history](/workspace/activity-log), which records the same saves as a timeline of who did what; the versions are the record itself, the activity is the log around it. ## Restore a version [step] 1. Find the version to put back and press **Restore this version**; the current version has no button, because it is already in place. 2. Confirm. The record as it stands is kept as a version too, so nothing is lost. The version's captured columns, the status included, are written back onto the artefact and a new version is written with the reason _restored_, naming the one put back. The flyout reloads with the restored record and `epic.restored`, `work_item.restored`, `requirement.restored`, `business_rule.restored`, `use_case.restored`, `raid_item.restored`, `decision.restored`, `glossary_term.restored`, `data_entity.restored`, `persona.version_restored` or `stakeholder.restored` goes out with both version numbers. A version identical to the current record is refused, since there is nothing to restore. <Callout type="warn" title="Approved and baselined artefacts"> A restore puts the version's status back with its fields, so an approved artefact restored to a draft version reads as a draft again and needs a new [approval](/planning/approvals) before a client guest sees it; the approval it had stays in its history. A restore that keeps the status marks the artefact changed since its approval, and the approval bar asks for a new submission. An artefact a frozen [baseline](/planning/baselines) holds cannot be restored at all: the restore is refused with the baseline named, until an approved [change request](/planning/change-requests) unlocks the artefact. </Callout> ## What a guest, a token and an agent see - A **client guest** never reads versions: they read the agreed record, and the versions are the drafts and the arguments behind it. The flyout shows them no Versions section. - A **token** reads an epic's versions through `GET /v1/epics/{epic}/revisions` and one version through `GET …/revisions/{revision}`, under `epic:view`, and restores one through `POST …/revisions/{revision}/restore` under `epic:update`; a work item's through the same three verbs under `/v1/work-items/{item}`, with `work-item:view` and `work-item:update`; a requirement's under `/v1/requirements/{requirement}`, with `requirement:view` and `requirement:update`; a rule's under `/v1/business-rules/{rule}`, with `business-rule:view` and `business-rule:update`; a use case's under `/v1/use-cases/{useCase}`, with `use-case:view` and `use-case:update`; a RAID item's under `/v1/raid-items/{raidItem}`, with `raid:view` and `raid:update`; a decision's under `/v1/decisions/{decision}`, with `decision:view` and `decision:update`; a term's under `/v1/glossary/{term}`, an entity's under `/v1/data-entities/{entity}`, a persona's under `/v1/personas/{persona}` and a stakeholder's under `/v1/stakeholders/{stakeholder}`, each with its own view and update abilities. - An **agent** reads versions with `list_revisions` and `get_revision` and restores one with `restore_revision`, which is destructive in the MCP sense and asks the client for confirmation. The tools need `activity:read` and the artefact's own ability, and refuse a guest token. ## What happens next - A restore is announced as `epic.restored`, `work_item.restored`, `requirement.restored`, `business_rule.restored`, `use_case.restored`, `raid_item.restored`, `decision.restored`, `glossary_term.restored`, `data_entity.restored`, `persona.version_restored` (`persona.restored` is the archive flag lifted) or `stakeholder.restored` with the version put back and the version written, so a mirror re-reads the record rather than guessing what moved. - An [approval](/planning/approvals) binds its request to the version under review and, when it carries, writes a version with the reason _approval_ and pins it on the artefact as the approved version; a [baseline](/planning/baselines) freezes a set of those approved versions on the same foundation and locks the artefacts it holds until a change request unlocks them. ## Heimdall on this page Try: “What changed on CRM-12 since it was approved?” · “Who moved the checkout epic to cancelled, and when?” · “Show me the version of the loyalty epic before Heimdall rewrote the goal.” Heimdall reads versions through `list_revisions` and `get_revision`. A restore it proposes is shown first and waits for your go-ahead, like every change it makes. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Read versions | The artefact's view permission (`epic:view`, `work-item:view`, `requirement:view`, `business-rule:view`, `use-case:view`, `raid:view`, `decision:view`, `glossary:view`, `data-entity:view`, `persona:view`, `stakeholder:view`), staff only | | Restore a version | The artefact's update permission (`epic:update`, `work-item:update`, `requirement:update`, `business-rule:update`, `use-case:update`, `raid:update`, `decision:update`, `glossary:update`, `data-entity:update`, `persona:update`, `stakeholder:update`) | ## Frequently asked <Accordions> <Accordion title="Does a restore delete the versions after the one restored?"> No. Nothing is ever deleted from a history while the artefact lives; a restore adds a version that names the one it put back. Only deleting the artefact removes its versions. </Accordion> <Accordion title="Why did my save write no version?"> It changed none of the captured columns: a reorder, for instance, or a save that resubmitted the same values. The activity history still records that the save happened. </Accordion> <Accordion title="Can I compare two versions that are not next to each other?"> Each version shows its diff from the one before. Read the two snapshots through the API or `get_revision` to compare any pair; a side-by-side compare in the flyout follows. </Accordion> </Accordions> ## Related - [Epics](/planning/epics) — the first artefact that carries versions - [Backlog](/planning/backlog) — work items, versioned with their acceptance criteria - [Functional requirements](/planning/functional-requirements) — requirements, versioned with their kind, category and measure - [Business rules](/planning/business-rules) — rules, versioned with their worked examples - [Use cases](/planning/use-cases) — use cases, versioned with their steps - [RAID log](/planning/raid-log) — risks, assumptions, issues and dependencies, their score derived again on every restore - [Decisions](/planning/decisions) — decisions, versioned with their options - [Glossary](/projects/glossary) — terms, versioned with their aliases and related terms - [Data dictionary](/projects/data-dictionary) — entities, versioned with their fields - [Personas](/projects/personas) — personas, versioned without their picture or archive flag - [Stakeholders](/projects/stakeholders) — stakeholders, versioned without their linked account - [Activity log](/workspace/activity-log) — the timeline around the versions - [Status reference](/reference/status-reference#revision-reason) — every revision reason and actor kind --- # Traceability Source: https://alm-docs.labs.zuselab.dev/planning/traceability By the end of this page you will have joined the artefacts of a project into one record that can be followed in both directions: from a requirement to the stories that implement it, from a story to the rules it honours and the decisions it builds on, from a bug to the story that fixes it; you will know every kind of link, what each reads like from either side, and what a link announces. A **link** joins two artefacts of the same project with a **kind** that is read from the source: _CRM-142 implements CRM-FR-12_. Opened from the other end, the same link reads the other way: _CRM-FR-12 is implemented by CRM-142_. Nine kinds of artefact can be linked: epics, work items, requirements, business rules, use cases, RAID items, decisions, data entities and glossary terms. Every link lives in the **Links** panel at the foot of the artefact's flyout, on every page that has one, so the record is read where the work is read. The panel is internal: a client guest never sees it, even on an artefact the guest may read. The traceability matrix that draws every link of a project on one grid lands with the test module; until then this panel and the API are the record. ## Before you begin | You need | Where it comes from | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | A project with two artefacts | Anything from the [planning](/planning) pages or the [dictionary](/projects/glossary): a link needs two ends in the same project. | | `link:view-any` | To see the panel and its rows; business analysts, project managers, developers, QA and viewers have it. | | `link:create` and `link:delete` | To add and remove links; the roles that write planning artefacts have both. | ## Open the links of an artefact [#open] 1. Open the artefact's page and open the artefact: its flyout opens from the row's menu, from **View**, or from a `?show=` link someone sent you. 2. Scroll to **Links**, below the artefact's versions. Each row is one link read from this side: a badge with the phrase (_implements_, _is blocked by_), a badge with the kind of the other artefact, the other artefact's key and title, the note when one was written, and a remove button when you may remove links. When nothing is linked yet the panel says so; every tree starts with one branch. ## Add a link [#add] 1. Press **Add Link**. The form opens inside the panel. 2. Pick the **Relation**: how this artefact relates to the one you are about to pick, read from here. The hint under the field reads the sentence back to you. 3. Pick the **Kind of Artefact** the other end is. 4. Type at least two characters of its key or title into **Find It**. The matches of that kind appear under the field, the artefact you are on left out; pick one under **Link To**. 5. Write a **Note** when the relation alone does not say why, and press **Link**. The row appears at the top of the list, a toast confirms the sentence that was written, and `link.created` goes out. **Cancel** closes the form and forgets what was typed. Three links are refused with a message: the same link twice (the same two ends with the same kind, in either direction for _relates to_ and _duplicates_, which read the same from both sides), an artefact linked to itself, and an artefact of another project. Anything else the picker offers can be linked. ## Open the other end [#follow] The key and title of each row are a link: it opens the other artefact's page with that artefact's flyout open, so a requirement's stories, a story's rules and a bug's fix are each one click away. Opened there, the same link is read from that side, with the phrase turned around. ## Remove a link [#remove] Press the remove button at the end of a row. The link goes, `link.deleted` goes out, and both artefacts stand as they were: removing a link never touches either end. There is no confirmation, because a link removed by mistake is added back in a few seconds and nothing else was lost. ## All the fields <Accordions> <Accordion title="Relation"> How this artefact relates to the other, read from this artefact: it _implements_ the other, _depends on_ it, _is fixed by_ it. One of the eleven kinds in the table below; _relates to_ is the default when nothing more precise applies. The relation is stored once and read from either side, so there is never a second link to keep in step. </Accordion> <Accordion title="Kind of artefact"> What the other end is: an epic, a work item, a requirement, a business rule, a use case, a RAID item, a decision, a data entity or a glossary term. Picking a kind narrows the search to that kind; changing it clears the match you had picked. </Accordion> <Accordion title="Find it"> A key such as `CRM-FR-12`, or a few words of the title; a data entity is found by its name and a glossary term by the term. At least two characters; up to eight matches appear, the artefact you are on never among them. </Accordion> <Accordion title="Link to"> The artefact picked from the matches. Required: the link needs an end. </Accordion> <Accordion title="Note"> Up to 300 characters on why the two belong together, shown on the row from both sides. Optional; most links say enough with their kind. </Accordion> </Accordions> ## The kinds of link | Read from the source | Read from the target | Use it for | | -------------------- | --------------------- | ------------------------------------------------------------------------------------------- | | **implements** | is implemented by | A story implements a requirement or a use case. | | **derives from** | is refined by | A requirement derives from a business rule or an epic. | | **verifies** | is verified by | A test case or a criterion verifies a requirement or a rule. | | **covers** | is covered by | A test case covers a story or a use case flow. | | **depends on** | is depended on by | The source cannot finish before the target does. | | **blocks** | is blocked by | The source stops the target from moving until it is done. | | **relates to** | relates to | The two belong together without one owning the other. | | **duplicates** | duplicates | The two say the same thing; keep one and close the other. | | **is caused by** | causes | A bug caused by a decision, an issue caused by a risk. | | **is fixed by** | fixes | A bug fixed by a story. | | **supersedes** | is superseded by | The source replaces the target, which stays for the record. | The [status reference](/reference/status-reference#link-kind) lists the codes the API and the tools use for each kind. ## What happens next - `link.created` and `link.deleted` go out with both ends' types and ids, the kind and the project, so a board, a wiki or a pipeline can keep its own picture in step; see the [`link.*` events](/webhooks/v1/events/link). - A link is not a version of either artefact and is never edited in place: to change its kind, remove it and add the right one. Neither artefact's [versions](/planning/revisions-and-versions) change when a link is added or removed, and the two events are the record of who linked what. - Heimdall and an agent connected over MCP read the links before they draft or build, so a story's requirements, rules and decisions reach them without being pasted; the context pack of a story will carry them when it lands. - The traceability matrix, the coverage columns and the dependency graph of later modules are all read from this one table: a link made today is drawn there without being made again. ## Managing - **Change a kind** by removing the link and adding it with the right relation; the two events say what changed. - **An artefact deleted** takes its links out of every list on both sides. - **A client guest** never sees the panel, whatever the artefact: links name internal items and stay internal. - **Many links at once** are made over the [REST API](/api/v1/reference/links) or by an agent with [`link_artefacts`](/mcp/v1/tools/planning/link-artefacts); the panel is one at a time on purpose. ## Heimdall on this page Heimdall reads the links of an artefact when it drafts for it or answers about it, and an agent connected over MCP links what it implements as it works. Neither draws links on its own: a link is a claim about the record, and the panel keeps that claim with the person who makes it. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | -------------------- | ---------------------------------------------------------- | | See the links | `link:view-any` and `link:view` on the project; never a client guest | | Add a link | `link:create` | | Remove a link | `link:delete` | ## Frequently asked <Accordions> <Accordion title="Which end is the source?"> The artefact you are on when you add the link. Pick the relation as a sentence that starts there: on a story, _implements_ the requirement; on the requirement, you would pick _is implemented by_ the story. Both write the same link, read from the side you open it on. </Accordion> <Accordion title="Why is the artefact I want not among the matches?"> The picker shows the artefacts of the picked kind in the same project whose key or title contain what you typed, eight at most: type more of the title, or the key itself, and check the kind. The artefact you are on is left out on purpose, and a client guest's view hides the picker altogether. </Accordion> <Accordion title="Where is the matrix?"> With the test module: the matrix draws requirements against stories and test cases with coverage colouring, and the links made here are its rows. Until it lands, the panel and `list_links` are the way to read the record. </Accordion> </Accordions> ## Related - [Backlog](/planning/backlog) — the stories, bugs and tasks most links start from - [Functional requirements](/planning/functional-requirements) — what the stories implement - [Business rules](/planning/business-rules) — what the stories honour - [Decisions](/planning/decisions) — what the epics build on - [Status reference](/reference/status-reference#link-kind) — every kind with its code - [`link.*` webhook events](/webhooks/v1/events/link) --- # Use Cases Source: https://alm-docs.labs.zuselab.dev/planning/use-cases By the end of this page you will have written down how each actor of a project reaches a goal with the system, step by step, as a use case a developer can build and a tester can walk; know every field a use case carries and what reads it downstream; and know how a use case moves from a draft to approved, how it is deleted, and where every version of it, steps included, is kept. A **use case** is one journey through the system: _Nadeesha, the returning shopper, redeems a voucher at checkout_. It names the persona who acts and anyone else in the room, the goal, what triggers it, what must be true before the first step and after the last, how often it happens, and the **steps**: what the actor does and what the system does in reply, in a **main flow** that goes right from the trigger to the goal, with **alternate flows** (another way to the goal) and **exception flows** (a failure the system must handle) that leave a numbered main step and carry a label such as `2a`. Use cases number on their own sequence (`CRM-UC-4`) and every save of one is kept as a [version](/planning/revisions-and-versions), steps included. A [requirement](/planning/functional-requirements) says what the system shall do; a use case says how a person gets something done with it, and the stories that cover a flow are drafted from its steps once the inline assists land. ## Before you begin | You want to | You need | | ----------------------- | -------------------------------------------------------------------------------------------------------------------- | | Open the Use Cases page | `use-case:view-any` on the project, which every default role carries. A client guest sees the agreed use cases only. | | Add a use case | `use-case:create`. Project managers and business analysts have it by default. | | Edit or move a use case | `use-case:update`. | | Delete a use case | `use-case:delete`. | | Name an actor | The persona to be on the project's [persona list](/projects/personas). | The page lives under a project: open the project and choose **Use Cases** in the sidebar, badged with how many use cases you may see. ## Read the use cases [step] The table lists one use case per row: | Column | What it shows | | ------------- | ------------------------------------------------------------------------------------- | | **Reference** | The use case's key, such as `CRM-UC-4`. | | **Use Case** | The title, with the goal beneath it. Press the title to open the detail flyout. | | **Actor** | The persona who acts, or _No actor named_. | | **Status** | Draft, in review, approved or deprecated, as a badge. Hidden from a guest. | | **Priority** | The MoSCoW letter: M, S, C or W. | | **Steps** | How many steps the use case carries, every flow counted. | | **Frequency** | How often it happens, in the words the analyst wrote, or _Not said_. | | **Updated** | How long ago the record last changed. | | Row menu | **View Use Case**, **Edit Use Case**, **Move to**, **Activity**, **Delete Use Case**. | Sort by reference, title, status, priority, frequency or updated by pressing a column heading; press it again to flip the direction. The list opens in the project's own order. Above the table, narrow the list with the search box (words in the reference, the title or the goal), the **status** and **priority** lists and the **actor** list, which offers the project's personas. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Add a use case [step] 1. Press **New Use Case**. 2. Type the **Title**: a verb and a goal, such as _Redeem a voucher at checkout_. This alone is enough to save; Heimdall will propose the rest from a persona and a goal. 3. Pick the **Actor** from the project's personas and the **Priority**, and name the **Other Actors** in the room, separated by commas. 4. Write the **Goal** and the **Trigger**, each in one line. 5. Under _Before and after_, write the **Preconditions**, the **Postconditions** and the **Frequency**. 6. Under _Steps_, press **Add Step** once per step and type what the actor does and the **System response**. A step of the main flow is numbered from one in the order it sits. For a branch, change the flow to _Alternate flow_ or _Exception flow_, type its **Label** (such as `2a`) and say which main step it **Leaves**. 7. Press **Create Use Case**. The use case takes the next number of the use cases' sequence (the first of `CRM` is `CRM-UC-1`), starts as a draft, appears in the table at once, writes its first version with its steps inside it, and `use_case.created` goes out to every endpoint subscribed to it. <Callout type="info" title="Write the main flow first, then the branches"> The main flow is the path where everything goes right, numbered one, two, three. Each thing that can go differently at a step is a branch that leaves that step: an alternate flow when the actor still reaches the goal another way, an exception flow when they do not. A branch that names no main step, or one the main flow does not have, is refused, so a flow never dangles. </Callout> ## Open a use case [step] Press a use case's title, or **View Use Case** in its row menu, to open the detail flyout: the reference, status and priority at the top, the title, then the actor and the other actors, the goal and the trigger, the preconditions and the postconditions, the steps with their numbers and labels (a branch says which flow it belongs to and which main step it leaves), the frequency and the dates, and the **Versions** section with every save of the use case. From the flyout: - **Edit** opens the editor on this use case. - **Move to** changes the status in one step, offering only the moves the lifecycle allows. - **Restore this version** under any earlier version puts it back, steps included; see [revisions and versions](/planning/revisions-and-versions). - **Activity** opens the use case's history: every change, who made it and where it came from. - **Delete** opens the confirmation. A link such as `/projects/<id>/use-cases?show=<use case id>` opens the page with that use case's flyout already open; Heimdall's **View** buttons use it. ## Edit a use case [step] 1. Press **Edit Use Case** in the row menu, or **Edit** in the flyout. 2. Change what you need, the step rows included: **Add Step** appends a row, the arrows reorder the rows and the cross removes one. The actor must be a persona of the project, and every branch must name a main step the flow has. 3. Press **Save Changes**. The save writes a new version of the use case, steps included, and `use_case.updated` goes out with the columns that changed, each with its old and new value. A save that changes only the steps writes a version and announces nothing, since no column of the record moved. The status does not change from the editor; it moves from the list or the flyout. ## Move a use case along its lifecycle [step] A use case reaches **In review** and **Approved** through its [approval](/planning/approvals) alone: **Submit for review** on the approval bar puts it in review, and the approvers' verdicts move it out, to approved when the request carries and back to where it stood when it is rejected or withdrawn. **Move to** in the row menu or the flyout offers the other moves the lifecycle allows from where the use case stands, and is refused while a request is open: | From | To | | -------------- | ----------------- | | **Draft** | Deprecated | | **In review** | Draft, deprecated | | **Approved** | Draft, deprecated | | **Deprecated** | Draft | An approval stamps when the use case was agreed and at which version, and a client guest sees it from that moment on and loses it when it is deprecated. Every move writes a version with the reason _moved_ and `use_case.status_changed` goes out with the status before and after; an approval writes one with the reason _approval_. ## Delete a use case [step] **Delete Use Case** in the row menu or **Delete** in the flyout opens the confirmation. Deleting removes the use case with its steps, every version it kept and its activity history; its number is never handed out again. `use_case.deleted` goes out. Prefer **Deprecated** for a use case the client withdrew: it keeps the record and the flows. ## All the fields <Accordions> <Accordion title="Title"> Required, 3 to 200 characters. What the use case is called, next to its reference, in the table, the stories drafted from it and Heimdall's answers. A verb and a goal read best. </Accordion> <Accordion title="Actor"> Optional. The persona who acts, from the project's personas; one of another project is refused. The flyout and the table show the persona's name, the API carries the id and the name, and the actor filter narrows the list to one persona's use cases. </Accordion> <Accordion title="Other actors"> Optional, up to ten names of at most 120 characters each. Anyone else in the room: another persona, a system, a third party. Typed as one line separated by commas, kept as a list, blanks and repeats dropped. </Accordion> <Accordion title="Goal and Trigger"> Optional, one line of up to 300 characters each. What the actor is trying to achieve, and what starts the use case. The goal is searchable and shown beneath the title. </Accordion> <Accordion title="Preconditions and Postconditions"> Optional rich text. What must be true before the first step, which a tester sets up and a developer guards, and what is true after the last step of the main flow, which a tester checks. Filtered to a safe subset when shown. </Accordion> <Accordion title="Frequency"> Optional, up to 120 characters, in words. How often the use case happens; it tells the team what to make fast and what to make safe, and the table sorts by it. </Accordion> <Accordion title="Priority"> Required, _should have_ by default. The MoSCoW priority: must have, should have, could have or won't have. The [status reference](/reference/status-reference#moscow-priority) says what each means. </Accordion> <Accordion title="Steps"> Optional, up to sixty rows, each with the flow it belongs to (main, alternate or exception), what the actor does (required, 3 to 500 characters), what the system does in reply (up to 500 characters), and for a branch its label (up to 20 characters) and the main step it leaves, counted from one. Ordered with the arrows; main steps are numbered in the order they sit, so a reorder renumbers them. Kept inside every version of the use case, sent as `steps` over the API and replaced in full through their own verb, and read by the stories drafted from a flow once the inline assists land. A row without an action, a branch without a main step, and a branch that names a step the main flow does not have are refused. </Accordion> <Accordion title="Status, reference, number, version and approved"> Read-only in the editor. The status moves from the list or the flyout, and through the [approval bar](/planning/approvals) for the review states; the reference and the number are assigned when the use case is created; the version counts the approvals that carried and stays at zero until the first; approved is stamped by the approval, with the version that was read. The flyout also reads the actor's name behind the id. </Accordion> </Accordions> ## What happens next - Every save, move and restore writes a [version](/planning/revisions-and-versions), steps included, and an entry in the project's [activity history](/workspace/activity-log). - `use_case.created`, `use_case.updated`, `use_case.status_changed`, `use_case.steps_changed`, `use_case.restored` and `use_case.deleted` go out to the webhook endpoints subscribed to the `use_case.*` family. - Stories will be drafted one per flow from a use case's steps, requirements and test cases will link to use cases, and the context pack of a story will carry the steps it covers, as those modules land. ## Heimdall on this page Heimdall reads use cases and their versions through the same tools the MCP server offers, and carries the open project as its context. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “Which use cases have no exception flow yet?” · “Walk me through CRM-UC-4 step by step and tell me where a tester would need test data.” · “Which of Nadeesha's use cases are still drafts?” The brief import will draft use cases with their flows into a review queue, and the inline assists will draft a story per flow from the steps. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------- | ----------------------------------------------------------------------------------- | | See the use cases | `use-case:view-any`, `use-case:view`; a client guest sees the agreed use cases only | | Add a use case | `use-case:create` | | Edit or move a use case | `use-case:update` | | Delete a use case | `use-case:delete` | | Read or restore its versions | `use-case:view` to read, `use-case:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="Is this a user story or a use case?"> A story is one slice of work the team builds and ships: _as a shopper I want to apply a voucher_. A use case is the whole journey the slices add up to, with the branches where it goes differently. Write the journey once and draft the stories from its flows, so every slice knows which step it serves. </Accordion> <Accordion title="Why does a guest not see my draft?"> A draft is the analyst's working note. A client guest sees a use case once it is approved and from then on, so what the client reads is always what was agreed. </Accordion> <Accordion title="What happens to the branches when I reorder the main steps?"> The main steps are renumbered in their new order, and a branch keeps the number it names, so check that each branch still leaves the step you meant before you save. </Accordion> </Accordions> ## Related - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [Personas](/projects/personas) — the people a use case is acted by - [Functional requirements](/planning/functional-requirements) — what the system shall do, which a use case walks through - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [Status reference](/reference/status-reference#use-case-flow) — every flow, status and priority - [`use_case.*` webhook events](/webhooks/v1/events/use-case) --- # Clients Source: https://alm-docs.labs.zuselab.dev/projects/clients By the end of this page you will have added a client, know every field it carries, and know how a client is archived when the relationship goes quiet and why one with projects is never deleted. A client is an organisation the team does projects for. A project names its client, so the client exists first; the registry's client list offers exactly the clients on this page. The page is **Clients** in the sidebar. ## Before you begin | You want to | You need | | ------------------------ | -------------------------------------------------------------------------------------------- | | See the clients | To belong to the team. A guest sees only the clients of the projects they were invited to. | | Add a client | `client:create` on your role, or to own the team. Admins, project managers and business analysts have it by default. | | Edit, archive or restore | `client:update`. | | Delete | `client:delete`, and no project may name the client. | ## Read the clients [step] | Column | What it shows | | ------------------- | ----------------------------------------------------------------------------------------------- | | **Client** | The name, with the legal name and industry beneath it. Press the name to open the flyout. | | **Status** | Prospect, active, dormant or churned, with an _Archived_ badge when the client is put away. | | **Projects** | How many projects you can see name the client. | | **Primary Contact** | The person who speaks for the client, with their email address. | | **Updated** | How long ago the record last changed. | | Row menu | **View Client**, **Edit Client**, **Archive Client** or **Restore Client**, **Activity**, **Delete Client**. | Sort by client, status or updated from the column headings. Narrow the list with the search box (words in the name, the legal name or the contact email), the **status** list and the **Archived** switch; **Clear Filters** on the empty state drops them all. ## Add a client [step] 1. Press **New Client**. 2. Type the **Name**. It is unique within the team regardless of case, and it is all a client needs to start. 3. Add the **Legal Name**, **Website**, **Industry**, **Country** and **Timezone** as you learn them. 4. Set the **Status**: _Prospect_ for a client you are talking to, _Active_ once work is in flight. 5. Name the **Primary Contact** and their email. 6. Write **Notes**: anything the team should know about working with the client. 7. Press **Create Client**. The client appears in the list at once, the registry's client list offers it, and `client.created` goes out to every endpoint subscribed to it. ## Open and edit a client [step] Press a client's name, or **View Client** in its row menu, to open the flyout: the status at the top, then the industry, website, country and timezone, the primary contact, the projects you can see done for the client (each a link to the registry), the notes and the timestamps. From the flyout, **Edit** opens the editor, **Archive** or **Restore** change whether the client is put away, **Activity** opens its history and **Delete** opens the confirmation. To change a client, press **Edit Client**, change what you need and press **Save Changes**. Every changed field travels in `client.updated` with its old and new value; an edit that changes nothing announces nothing. ## Archive, restore and delete [step] - **Archive Client** takes the client out of the default view and lists it under the **Archived** switch. Its projects and its history stay exactly as they were. - **Restore Client** brings it back. - **Delete Client** removes the client for good, with its history. It is refused while any project, archived or deleted ones included, names the client: the confirmation says how many and offers archiving instead, or move the projects to another client first. <Callout type="info" title="Status and archive are two different things"> The status says where the relationship stands and is set by hand; a churned client may well stay live on the page while its last project is closed out. Archiving is what takes a client out of the way, and it keeps the status it had. </Callout> ## All the fields <Accordions> <Accordion title="Name"> Required, 2 to 100 characters, unique within the team regardless of case. The short name everyone uses, shown on every project done for the client. </Accordion> <Accordion title="Legal Name"> Optional, up to 160 characters. The registered name, for documents and sign-off sheets. </Accordion> <Accordion title="Website"> Optional, a full address such as `https://acme.example`. </Accordion> <Accordion title="Industry"> Optional free text, up to 80 characters. </Accordion> <Accordion title="Country"> Optional, a two-letter code such as `LK` or `GB`, upper-cased as you type. </Accordion> <Accordion title="Timezone"> Optional, picked from the list. Shown on the client's record for the team planning calls and release windows. </Accordion> <Accordion title="Status"> Required. Prospect, active, dormant or churned; the [status reference](/reference/status-reference#client-status) says what each means. </Accordion> <Accordion title="Primary Contact and Email"> Optional. Who speaks for the client; the address is lower-cased. Stakeholders with approval rights are added on each project. </Accordion> <Accordion title="Notes"> Optional rich text, filtered to a safe subset when shown. </Accordion> <Accordion title="Projects, created, updated and archived stamps"> Read-only in the flyout: the projects you can see done for the client, how many name it in all, and the stamps the application keeps, shown in your own timezone. </Accordion> </Accordions> ## What happens next - Every create, edit, archive, restore and delete is written to the client's [activity history](/workspace/activity-log). - The `client.created`, `client.updated`, `client.archived`, `client.restored` and `client.deleted` events go out to every endpoint subscribed to them. - A new client is offered in the registry's client list at once. ## Heimdall on this page Heimdall reads the clients through the same tools the MCP server offers, and its answers carry a **View** button that opens the client's flyout. Try: “Which clients do we work for?” · “Who is the primary contact at Acme Retail?” · “Which clients are dormant?” Adding and changing clients is a person's work: Heimdall and an agent over MCP read the clients and never write to them. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ------------------------- | -------------------------------------- | | See the clients | `client:view-any` | | Open a client | `client:view` | | Add a client | `client:create`, or owning the team | | Edit, archive or restore | `client:update` | | Delete a client | `client:delete` | ## Frequently asked <Accordions> <Accordion title="Why is the delete button disabled?"> A project still names the client. Archive the client instead, or move the projects to another client from the registry first. </Accordion> <Accordion title="As a guest I see only one client"> A guest sees the clients of the projects they were invited to and no other, and cannot change any of them. </Accordion> </Accordions> ## Related - [The registry](/projects/registry) — the projects done for a client - [Members and guests](/workspace/members-and-guests) — inviting a client's stakeholder - [Status reference](/reference/status-reference#client-status) --- # Components Source: https://alm-docs.labs.zuselab.dev/projects/components By the end of this page you will have written down what a project is made of, know every field a component carries and what reads each one downstream, and know how a component is put away, brought back and, when nothing runs it any more, deleted. A project is rarely one program. The customer portal is a web app, an API behind it, an iPhone app and an Android app, a nightly import service and the platform they all run on. Each of those is a **component**: a named application with a kind, a slug, the technologies it is built with and the person who answers for it. Environments say which component they run, stories and requirements will tag the components they touch, releases will carry a version per component and test runs will name the one under test, so the record every later phase reads starts here. ## Before you begin | You want to | You need | | ----------------------------- | ---------------------------------------------------------------------------------------------------------- | | Open the Components page | `component:view-any` on the project. Every staff role has it; a client guest is not shown the page. | | Add a component | `component:create`, through your role on the project or your team role, or to own the team. | | Edit, archive or restore one | `component:update`. | | Delete one | `component:delete`, and no environment may still run the component. | | Name an owner | The owner to be a member of the team; invite them on the [Members](/workspace/members-and-guests) page first. | The page lives under a project: open the project from the selector or the registry and choose **Components** in the sidebar's **Selected Project** group. The entry is badged with the number of live components. ## Read the components [step] The table lists one component per row, in the project's order: | Column | What it shows | | ---------------- | -------------------------------------------------------------------------------------------------------- | | **Component** | The name, with the slug beneath it in monospace. Press the name to open the detail flyout. An archived component carries an _Archived_ badge. | | **Kind** | What the component is: a web app, an API, a mobile app, a service and so on, with its icon. | | **Stack** | The first three technologies it is built with, and a count of the rest. | | **Environments** | How many environments run the component. | | **Owner** | The team member who answers for it, or _Unowned_. | | **Updated** | How long ago the record last changed. | | Row menu | **View Component**, **Edit Component**, **Archive Component** or **Restore Component**, **Activity**, **Delete Component**. | Sort by component, kind or updated by pressing a column heading; press it again to flip the direction. Above the table, narrow the list with the search box (words in the name or the slug), the **kind** list and the **Archived** switch, which shows archived components instead of live ones. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Add a component [step] 1. Press **New Component**. 2. Type the **Name**: what the team calls this application, such as _Customer Web App_ or _Public API_. The name is unique within the project. 3. Pick the **Kind**. A web app, an API or a service starts on the house stack: the moment you pick one of those kinds with an empty stack, the **Tech Stack** field is filled with Laravel, Livewire, Flux, Tailwind, PostgreSQL and Valkey, ready to be trimmed or extended. 4. Leave the **Slug** empty unless you want a particular one. It is derived from the name (`customer-web-app`), and when that is taken a numbered variant is used. 5. Describe what the application does and who uses it in the **Description**, and name the **Owner** if one is agreed. 6. Press **Create Component**. The component appears in the table at once and `component.created` goes out to every endpoint subscribed to it. <Callout type="info" title="A name is all it needs"> Everything else has a default or can follow later. Heimdall reads the tech stack when it drafts stories, tests and estimates for the component, so filling it in early pays off, but an empty stack never blocks anything. </Callout> ## Open a component [step] Press a component's name, or **View Component** in its row menu, to open the detail flyout: the name with the kind beside it, then the **Identity** block (name, slug, kind), the **Stack and Owner** block, the **Description** as written, the **Runs In** list of the environments that run the component with their kind, address and health, and the created and updated stamps. From the flyout, **Edit**, **Archive** or **Restore**, **Activity** and **Delete** are one press away. A link such as `/projects/<project>/components?show=<id>` opens the page with that component's flyout already open; notifications and Heimdall's buttons use it. ## Edit a component [step] 1. Press **Edit Component** in the row menu, or **Edit** in the flyout. 2. Change what you need. The same rules as adding apply: the name stays unique within the project, a typed slug must be free, the owner must be a member of the team. 3. Press **Save Changes**. Every change travels as `component.updated` with its old and new values. An edit that changes nothing announces nothing. ## Archive, restore and delete [step] - **Archive Component** takes the component out of the default view. Its environments and its history are kept and stay readable; switch on **Archived** above the table to find it. Use it for the admin tool that was retired or the mobile app the client dropped from scope. - **Restore Component** brings an archived component back into the default view. - **Delete Component** removes the component with its activity history. It is refused while an environment still runs the component: archive the component instead, or move those environments to another component first. The confirmation names the consequence; there is no undo. ## All the fields <Accordions> <Accordion title="Name"> Required, 2 to 80 characters, unique within the project regardless of case. Shown wherever the component is named: this page, the environment that runs it, the component tags on stories and requirements, and release notes. </Accordion> <Accordion title="Slug"> Optional on entry, always present on the record. Lower-case letters, digits and hyphens, starting with a letter, 2 to 40 characters, unique within the project: `web`, `api`, `mobile-ios`. Derived from the name when left empty, with a numbered variant when the derived slug is taken. It names the component in version strings such as `web@1.4.0`. </Accordion> <Accordion title="Kind"> Required. Web app, API, iOS app, Android app, cross-platform app, desktop app, service, integration, database, infrastructure or other. The kind picks the icon and, for a web app, an API or a service, offers the house stack when the stack is empty. </Accordion> <Accordion title="Description"> Optional rich text, up to 20,000 characters: what this application does and who uses it. Filtered to a safe subset when shown. Heimdall reads it with the project's own description. </Accordion> <Accordion title="Tech Stack"> Optional, up to 20 technologies of up to 40 characters each, typed separated by commas and kept as a list of tags. Duplicates are dropped regardless of case. Heimdall reads the tags when it drafts stories, tests and estimates for this component. </Accordion> <Accordion title="Owner"> Optional, a member of the team. The person who answers for the application; the flyout and the table show their name. Leave it empty for _Unowned_. </Accordion> <Accordion title="Environments, archived, created and updated"> Read-only. The flyout lists the environments that run the component and the table counts them; the stamps are kept by the application and shown in your own timezone. </Accordion> </Accordions> ## What happens next - Every add, edit, archive, restore and delete is written to the component's [activity history](/workspace/activity-log) with who did it and where from. - The `component.created`, `component.updated`, `component.archived`, `component.restored` and `component.deleted` events go out to every endpoint subscribed to them. - An [environment](/projects/environments) can name the component it runs, and a component with environments cannot be deleted until they are moved or removed. - Stories, requirements, releases and test runs will name components as those modules land, which is why the slug stays fixed while the row exists. ## Heimdall on this page Heimdall reads the components through the same tools the MCP server offers and carries the open project as its context, so a question needs no key. Try: “What is this project made of?” · “Which components have no owner yet?” · “What is the API component built with?” Shaping components is a person's work: Heimdall and an agent over MCP read the record and never write to it. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ------------------------------- | -------------------------------------------------------------- | | See the components | `component:view-any` | | Open a component | `component:view` | | Add a component | `component:create`, or owning the team | | Edit, archive or restore | `component:update`, through a project role or a team role | | Delete a component | `component:delete`, and no environment may run the component | ## Frequently asked <Accordions> <Accordion title="Why was my slug changed?"> You left it empty and the slug derived from the name was already used by another component of the project, so a numbered variant was taken. Type the slug you want in the editor; a typed slug is refused rather than changed when it is taken. </Accordion> <Accordion title="Delete is refused"> An environment still runs the component. Archive the component instead, which keeps the environments and the history, or move the environments to another component and delete again. </Accordion> <Accordion title="One codebase ships as a web app and an API"> Record one component of the kind that matters most to the client, usually the web app, and put both roles in its description. A separate API component is for an API other systems call on its own, with its own version and environments. </Accordion> </Accordions> ## Related - [Environments](/projects/environments) — where each component runs - [The project overview](/projects/overview) — the project the components belong to - [Activity log](/workspace/activity-log) — where every change is recorded - [`component.*` webhook events](/webhooks/v1/events/component) --- # Data Dictionary Source: https://alm-docs.labs.zuselab.dev/projects/data-dictionary By the end of this page you will have written down what a project keeps records of, field by field, know every field an entity and a data field carry and what reads each one downstream, and know how an entity moves through its review and how it is removed. A **data entity** is a thing the project keeps records of: a customer, an order, a branch, a shipment. Each carries its **fields** with their type, flags, validation and an example, the component whose records they are, the member who answers for it, whether it holds personal data and how long it is kept. Entities and fields are the data dictionary: requirements and business rules will name them, the API contracts and the database designs are read against them, and Heimdall reads them when it drafts stories, rules and test data. The Data Entities tab is the second of the project's **Dictionary** pages; the first is the [glossary](/projects/glossary), the words the project agrees on. ## Before you begin | You want to | You need | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | Open the Data Entities tab | `data-entity:view-any` on the project. Every staff role has it; a client guest has it through their project membership and sees the approved entities only. | | Create an entity | `data-entity:create`, through your role on the project or your team role, or to own the team. Business analysts have it by default. | | Edit or move an entity, or change its fields | `data-entity:update`. | | Delete an entity | `data-entity:delete`. | The page lives under a project: open the project, choose **Dictionary** in the sidebar and switch to **Data Entities** in the tab strip above the table. ## Read the entities [step] The table lists one entity per row, in the project's order: | Column | What it shows | | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **Entity** | The name with the plural beneath it, and a _Personal data_ badge when the entity holds data about people. Press the name to open the detail flyout. | | **Component** | The component whose records these are, or _Whole project_. | | **Owner** | The team member who answers for the entity, or _Unowned_. | | **Fields** | How many fields the entity carries. | | **Status** | Where the entity stands in its review: draft, in review, approved or deprecated, as a coloured badge. Staff only. | | **Updated** | How long ago the record last changed. | | Row menu | **View Entity**, **Edit Entity**, **Move to** (the statuses the entity may move to), **Activity**, **Delete Entity**. | Sort by entity, status or updated by pressing a column heading; press it again to flip the direction. Above the table, narrow the list with the search box (words in the name, the plural or the description), the status filter (staff only), the component filter and the **Personal data** switch, which shows the entities that hold personal data only. The filters travel in the page address; **Clear Filters** on the empty state drops them all. A client guest sees the approved entities only, without the status column or the status filter. ## Create an entity [step] 1. Press **New Entity**. 2. Type the **Name** in the singular, unique within the project, and the **Plural** when it is not the name with an s. 3. Pick the **Component** whose records these are, or leave it empty when the whole project shares them, and the **Owner** who answers for the entity. 4. Write the **Description**: what the entity is and what it is for, in a sentence or two. 5. Under **Data Handling**, switch on **Personal data** when the entity holds data about identifiable people, and write the **Retention**: how long records are kept and why. 6. Under **Fields**, press **Add Field** once per field and fill each row: the **Name**, the **Type**, what it **Points At** for a reference, its **Values** for an enum, the **Required**, **Unique** and **Personal data** flags, an **Example**, the **Validation** rule in words and a **Description**. The arrows move a row up or down; the cross removes it. 7. Press **Create Entity**. The entity appears in the table at once as a draft; `data_entity.created` goes out to every endpoint subscribed to it, followed by `data_entity.fields_changed` when the entity was created with fields. <Callout type="info" title="A name is all it needs"> An entity can start as a bare name; the fields, the owner and the data handling follow as the analyst learns the domain, and Heimdall can draft the fields from the brief for review. Two rules hold from the start: the name is unique within the project, and no two fields of one entity share a name. </Callout> ## Open an entity [step] Press an entity's name, or **View Entity** in its row menu, to open the detail flyout: the status and personal-data badges, the name with its plural, component and owner, the description, the retention note, the **Fields** as a table (name and description, type with the entity a reference points at or the values of an enum, the flags, the example and the validation rule) and the created and updated stamps. From the flyout, **Edit**, **Move to**, **Activity** and **Delete** are one press away. A link such as `/projects/<project>/dictionary/entities?show=<id>` opens the tab with that entity's flyout already open; notifications and Heimdall's buttons use it. ## Edit an entity and its fields [step] 1. Press **Edit Entity** in the row menu, or **Edit** in the flyout. 2. Change what you need on the entity, and work the field rows: add, reorder, change or remove them. A new name must be free within the project. 3. Press **Save Changes**. A change to the entity itself travels as `data_entity.updated` with its old and new values; a change to the rows travels as `data_entity.fields_changed` with how many fields were added, changed and removed. An edit that changes nothing announces nothing. The status does not change here; it moves from the list or the flyout. ## Move an entity along its review [step] An entity starts as a **draft**. Put it **in review** when the people who approve it should read it, mark it **approved** once they agree, and **deprecate** it when the project stops keeping those records. Open **Move to** in the row menu or the flyout and choose the status; a deprecated entity offers only the way back to draft. The move is written to the entity's history and travels as `data_entity.status_changed` with the status before and after. A client guest sees an entity while it is approved and not otherwise, so approving an entity is also what shares it with the client. ## Delete an entity [step] **Delete Entity** in the row menu or **Delete** in the flyout removes the entity with its fields and its activity history after a confirmation; there is no undo. A reference field of another entity that pointed at it loses its target and keeps its name. Requirements and business rules that will name entities are not here yet; once they are, an entity they name will be deprecated rather than deleted. ## All the fields <Accordions> <Accordion title="Name and Plural"> The name is required, 2 to 80 characters, in the singular and unique within the project regardless of case; the plural is optional, up to 80 characters, for when it is not the name with an s. The contracts and the database designs use both. </Accordion> <Accordion title="Component"> Optional, one of the project's live components. The component whose records these are; leave it empty when the whole project shares them. The component filter above the table reads it. </Accordion> <Accordion title="Owner"> Optional, a member of the team. The person who answers for the entity and its data; the privacy and retention questions go to them. </Accordion> <Accordion title="Description"> Optional rich text, up to 20,000 characters, written for the whole project. Heimdall reads it when it drafts stories and rules that touch the entity. </Accordion> <Accordion title="Personal data and Retention"> The switch says whether the entity holds data about identifiable people; the retention note, up to 255 characters, says how long records are kept and why. The privacy requirements and the data-protection register read both. </Accordion> <Accordion title="Fields"> Up to a hundred rows, in the order they should keep. Each row carries a **Name** (required, up to 80 characters, unique within the entity regardless of case), a **Type** (below), **Points At** for a reference (one of the project's entities), **Values** for an enum (up to fifty of up to 80 characters each, typed separated by commas, at least one), the **Required**, **Unique** and **Personal data** flags, an **Example** and a **Validation** rule (each up to 255 characters) and a **Description** (up to 2,000 characters). A row that already exists keeps its id, so the history and the API see a change rather than a removal and an addition. | Type | Code | What it holds | | ----------------- | ----------- | -------------------------------------------------------------- | | **String** | `string` | A short line of text: a name, a code, an address line. | | **Text** | `text` | Longer text: a description, a note, a comment. | | **Integer** | `integer` | A whole number: a count, a quantity, a year. | | **Decimal** | `decimal` | A number with decimals: money, a weight, a rate. | | **Boolean** | `boolean` | Yes or no. | | **Date** | `date` | A calendar date without a time. | | **Date and time** | `datetime` | A moment in time, with its timezone. | | **Enum** | `enum` | One of a fixed list of values; list them in the field. | | **UUID** | `uuid` | An identifier the system makes. | | **JSON** | `json` | A structured blob the system reads as a whole. | | **File** | `file` | An uploaded file or an image. | | **Reference** | `reference` | A link to a row of another entity; name the entity. | </Accordion> <Accordion title="Status"> _Draft_, _In review_, _Approved_ or _Deprecated_; a new entity starts as a draft. Moved from the list or the flyout, never from the editor. The [status reference](/reference/status-reference#artefact-status) says what each step means and which moves are allowed. </Accordion> <Accordion title="Component, owner, created and updated"> Read-only facts the flyout shows: the component's and the owner's names as they are now, and the stamps kept by the application in your own timezone. </Accordion> </Accordions> ## What happens next - Every create, edit, move, change to the fields and delete is written to the entity's [activity history](/workspace/activity-log) with who did it and where from. - The `data_entity.created`, `data_entity.updated`, `data_entity.status_changed`, `data_entity.fields_changed` and `data_entity.deleted` events go out to every endpoint subscribed to them. - Requirements, business rules and the API contracts will name entities and fields as those modules land, which is why an entity in use is deprecated rather than deleted. ## Heimdall on this page Heimdall reads the dictionary through the same tools the MCP server offers and carries the open project as its context. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “Which entities hold personal data, and how long do we keep them?” · “List the fields of the Order entity with their types.” · “Draft the fields a Customer entity needs from the brief.” Writing the dictionary is a person's work, with Heimdall drafting for review: an agent over MCP reads the entities and their fields to design a table, a form or a payload, and never writes to them. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | --------------------------------------- | -------------------------------------------------------------- | | See the entities | `data-entity:view-any`; a client guest sees the approved ones | | Open an entity | `data-entity:view` | | Create an entity | `data-entity:create`, or owning the team | | Edit or move an entity, change its fields | `data-entity:update`, through a project role or a team role | | Delete an entity | `data-entity:delete` | | Read or restore its versions | `data-entity:view` to read, `data-entity:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="Is this the database design?"> No. The dictionary says what the business keeps records of and what each record carries, in the project's words; the developers design the tables against it, and a field here may become two columns or a whole table there. Keep it at the level the client would recognise. </Accordion> <Accordion title="Can a guest see the entities?"> Yes, the approved ones with their fields. The client reads what their system will keep records of and can say where the fields are wrong; drafts and entities in review stay with the team until they are approved. </Accordion> <Accordion title="What does a reference field point at when its entity is deleted?"> Nothing: it keeps its name and loses its target. Edit the field to point at another entity, or remove it, when the dictionary moves on. </Accordion> </Accordions> ## Related - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [Glossary](/projects/glossary) — the words the project agrees on - [Components](/projects/components) — the applications whose records the entities are - [The project overview](/projects/overview) — the project the dictionary belongs to - [Status reference](/reference/status-reference#artefact-status) — the review lifecycle - [Revisions and versions](/planning/revisions-and-versions) — every save kept, the fields with it, any version put back - [`data_entity.*` webhook events](/webhooks/v1/events/data-entity) --- # Design References Source: https://alm-docs.labs.zuselab.dev/projects/design-references By the end of this page you will have linked a project's designs where everyone finds them, know every field a design reference carries and what reads each one downstream, and know how a reference is attached to a component, changed and removed. A **design reference** is a link the project keeps to one of its designs: a Figma file, a wireframe, a prototype, a screenshot, an API contract, a sequence diagram, an entity diagram or a document. The link is the record and the file stays where the designer keeps it. Developers and testers open the reference before they build or check a screen, the client signs off against it, and Heimdall names it when it drafts a story for the screen it shows. A reference hangs off the whole project or off one of its [components](/projects/components), so the web app's screens sit beside the web app and the API contract beside the API. Epics, stories, requirements, use cases and test cases will take references of their own as those modules land. ## Before you begin | You want to | You need | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Open the Designs page | `design-reference:view-any` on the project. Every staff role has it; a client guest has it through their project membership. | | Add a reference | `design-reference:create`, through your role on the project or your team role, or to own the team. Business analysts have it by default. | | Edit a reference | `design-reference:update`. | | Delete a reference | `design-reference:delete`. | The page lives under a project: open the project and choose **Designs** in the sidebar. ## Read the designs [step] The page shows one card per reference, in the project's order: | On the card | What it shows | | ----------------- | -------------------------------------------------------------------------------------------------------------- | | Kind badge | What the link points at, as a coloured badge: Figma, wireframe, prototype, screenshot, API contract, sequence diagram, entity diagram, document or other. | | Title and host | The reference's title, which opens the detail flyout, and the host the address points at. | | Attached to | _Whole project_ or the component the design belongs to, the version when one is recorded, and how long ago the reference changed. | | **Open Design** | Opens the design in a new tab, exactly where the designer keeps it. | | Card menu | **View Reference**, **Edit Reference**, **Activity**, **Delete Reference**. | Above the cards, narrow the page with the search box (words in the title, the address or the version), the kind filter and the component filter. The filters travel in the page address; **Clear Filters** on the empty state drops them all. A client guest sees every reference of their project: the designs are what the client reviews and signs off against. ## Add a reference [step] 1. Press **Add Reference**. 2. Type the **Title** as the team refers to the design, and paste the **Address** where it lives. 3. Under **What It Is**, pick the **Kind** and, when the design belongs to one component, choose it under **Attached To**; leave it empty for a design that covers the whole project. Record the **Version** the designer labels it with. 4. Write the **Notes**: what to look at and why, which frames are signed off, which are still exploring. 5. Press **Add Reference**. The card appears at once, you are recorded as the person who added it, and `design_reference.created` goes out to every endpoint subscribed to it. <Callout type="info" title="The link is the record"> Nothing is uploaded. The design stays in Figma, in the wiki, in the contract repository, wherever the designer keeps it; the reference says what it is, which version is current and what to look at. Attachments and an in-app viewer for API contracts follow with the attachments block. </Callout> ## Open a reference [step] Press a card's title, or **View Reference** in its menu, to open the detail flyout: the kind badge, the title and the full address, **Open Design**, what it is **Attached To**, the **Version**, who **Added** it, the **Notes** and the created and updated stamps. From the flyout, **Edit**, **Activity** and **Delete** are one press away. A link such as `/projects/<project>/designs?show=<id>` opens the page with that reference's flyout already open; notifications and Heimdall's buttons use it. ## Edit a reference [step] 1. Press **Edit Reference** in the card menu, or **Edit** in the flyout. 2. Change what you need: the address when the design moved, the version when a new one is current, the component it is attached to when it was filed wrongly. 3. Press **Save Changes**. Every change travels as `design_reference.updated` with its old and new values; moving a reference between the project and a component shows as the attachment changing. An edit that changes nothing announces nothing. ## Delete a reference [step] **Delete Reference** in the card menu or **Delete** in the flyout removes the link with its activity history after a confirmation; there is no undo. The design itself stays where the designer keeps it. When a component is deleted, its references are moved onto the whole project rather than lost. ## All the fields <Accordions> <Accordion title="Title"> Required, 2 to 120 characters. What the design is called as the team refers to it; the card, the flyout and the events carry it. </Accordion> <Accordion title="Address"> Required, an `http` or `https` link of up to 2,048 characters. Where the design lives; the card shows its host and **Open Design** opens it in a new tab. </Accordion> <Accordion title="Kind"> Required, one of nine. The [status reference](/reference/status-reference#design-reference-kind) says what each kind is for; the badge colour groups the cards at a glance and the kind filter reads it. </Accordion> <Accordion title="Attached To"> Optional, one of the project's live components. The design belongs to that component; leave it empty when it covers the whole project. The component filter reads it, and the flyout shows the component's name. </Accordion> <Accordion title="Version"> Optional, up to 40 characters. The version or revision as the designer labels it, so a developer knows which frames are current; the search box reads it. </Accordion> <Accordion title="Notes"> Optional rich text, up to 20,000 characters. What to look at and why; read by the developers and the testers before they open the design. </Accordion> <Accordion title="Added by, created and updated"> Read-only facts the flyout shows: who added the reference, and the stamps kept by the application in your own timezone. </Accordion> </Accordions> ## What happens next - Every add, edit and delete is written to the reference's [activity history](/workspace/activity-log) with who did it and where from. - The `design_reference.created`, `design_reference.updated` and `design_reference.deleted` events go out to every endpoint subscribed to them. - Stories, requirements, use cases and test cases will carry references of their own as those modules land, and an uploaded API contract will open in an in-app viewer with the attachments block. ## Heimdall on this page Heimdall reads the references through the same tools the MCP server offers and carries the open project as its context. Try: “Which designs does the web app have?” · “Is there an API contract for the payments service, and which version?” · “Which references have no version recorded?” Linking designs is a person's work: an agent over MCP reads the references to build against the design and never rewrites the links. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | --------------------- | ------------------------------------------------------------------- | | See the references | `design-reference:view-any`, a client guest included | | Open a reference | `design-reference:view` | | Add a reference | `design-reference:create`, or owning the team | | Edit a reference | `design-reference:update`, through a project role or a team role | | Delete a reference | `design-reference:delete` | ## Frequently asked <Accordions> <Accordion title="Can I upload the design file itself?"> Not yet. The reference is a link, which keeps one source of truth where the designer works; uploads and an in-app viewer for API contracts arrive with the attachments block. </Accordion> <Accordion title="What happens to a component's designs when the component goes?"> They are moved onto the whole project, with their titles, versions and notes, so nothing is lost; re-attach them to another component when there is one. </Accordion> <Accordion title="Can a guest see the designs?"> Yes, every reference of their project, notes included. The designs are what the client reviews and signs off against, so the client reads the same links the team builds from. </Accordion> </Accordions> ## Related - [Components](/projects/components) — what a reference may be attached to - [The project overview](/projects/overview) — the project the designs belong to - [Status reference](/reference/status-reference#design-reference-kind) — the nine kinds - [`design_reference.*` webhook events](/webhooks/v1/events/design-reference) --- # Environments Source: https://alm-docs.labs.zuselab.dev/projects/environments By the end of this page you will have registered where a project runs, know every field an environment carries and what reads each one downstream, know how the scheduled health check works and how a change freeze is declared and lifted, and know which environments a client guest is shown. An **environment** is one named place a project's software runs: a developer's machine, the shared development server, QA, staging, the UAT the client signs off on, pre-production and production. Each has a kind, the component it runs or the whole project, the addresses people and pipelines open, the branch deployed there, two flags that will govern deployments and the health the last check found. Deployments will land on environments, releases will promote through the kinds in order and incidents will name the one that failed, so the record every later phase reads starts here. ## Before you begin | You want to | You need | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Open the Environments page | `environment:view-any` on the project. Every staff role has it; a client guest has it through their project membership. | | Add an environment | `environment:create`, through your role on the project or your team role, or to own the team. | | Edit one, or check its health | `environment:update`. | | Freeze or lift a freeze | `environment:freeze`. Project managers, release managers and admins have it by default. | | Delete one | `environment:delete`. | | Name the component it runs | The component to exist already; add it on the [Components](/projects/components) page first. | The page lives under a project: open the project from the selector or the registry and choose **Environments** in the sidebar's **Selected Project** group. The entry is badged with the number of environments you may see. ## Read the environments [step] The table lists one environment per row, in the project's order: local first, then development, QA, staging, UAT, pre-production, production and custom, and within a kind in the order they were added. | Column | What it shows | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **Environment** | The name, with the component it runs (or _Whole project_) and its address beneath it. Press the name to open the detail flyout. | | **Kind** | Local, development, QA, staging, UAT, pre-production, production or custom, as a coloured badge. | | **Health** | Healthy, degraded, down or not checked, with how long ago the last check ran. | | **Branch** | The branch deployed there. Staff only. | | **Flags** | _Frozen_ while a change freeze is in force, _Protected_ when a deployment there needs a promotion approval, _Auto deploy_ when the pipeline deploys there without a hand. | | **Updated** | How long ago the record last changed. | | Row menu | **View Environment**, **Edit Environment**, **Check Health Now**, **Freeze Changes** or **Lift Freeze**, **Activity**, **Delete Environment**. | Sort by environment, kind, health or updated by pressing a column heading; press it again to flip the direction. Above the table, narrow the list with the search box (words in the name or an address), the **kind** list, the **component** list and the **health** list. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Add an environment [step] 1. Press **New Environment**. 2. Type the **Name** and pick the **Kind**. The name is unique within the project and the component: _Staging_ for a shared staging, _API Staging_ when the API has its own. 3. Pick the **Component** the environment runs, or leave _Whole project_ when every component shares it. 4. Add the **Addresses** you know: what people open, the API, the back office, and the address the scheduled health check should call. 5. Under **Deployment**, name the **Branch** deployed there and the **Current Version** running now, say where it is hosted and in which region, and set **Auto deploy** and **Protected**. 6. Put access notes, quirks and who to call in the **Notes**. Never a secret: the record is read by everyone who sees the project. 7. Press **Create Environment**. The environment appears in the table at once and `environment.created` goes out to every endpoint subscribed to it. The usual set is development, staging, UAT and production; add the rest as the project needs them. <Callout type="info" title="A name and a kind are enough"> The addresses, the branch and the flags can follow once the environment exists. A health check starts the moment its address is saved and stops the moment it is cleared. </Callout> ## Open an environment [step] Press an environment's name, or **View Environment** in its row menu, to open the detail flyout: the name with the component beneath it, the kind, health and flag badges, then **Addresses** (address, API address, admin address), **Deployment** (branch, current version, hosting, region), **Health** (when it was last checked, the streak of failed checks when there is one, or _No health check address_), the **Change Freeze** and its end, the **Notes** and the created and updated stamps. From the flyout, **Edit**, **Check Now**, **Freeze** or **Lift Freeze**, **Activity** and **Delete** are one press away. A link such as `/projects/<project>/environments?show=<id>` opens the page with that environment's flyout already open; notifications and Heimdall's buttons use it. ## Edit an environment [step] 1. Press **Edit Environment** in the row menu, or **Edit** in the flyout. 2. Change what you need. The same rules as adding apply: the name stays unique within the project and component, every address is a full URL, the component is one of the project's. 3. Press **Save Changes**. Every change travels as `environment.updated` with its old and new values. An edit that changes nothing announces nothing. ## The health check [step] An environment with a **Health Check Address** is checked on a schedule. Every few minutes the application queues a check for every environment whose last check is older than the interval, and each check opens the address and reads the answer: | The address answers | Health | | ----------------------------------- | ------------ | | A success status (2xx) | **Healthy** | | A server error (5xx), or nothing within the timeout | **Down** | | Anything else: a redirect, a 404, a 401 | **Degraded** | The answer is written with the time it was read, and the **Health** column and badge update. A change from one health to another is announced as `environment.health_changed` with the previous and the new health; the same answer twice in a row announces nothing. A streak of checks that found the environment down is counted and shown in the flyout, and reset by the first answer that is not down; the maintenance module will read the streak to open an incident when production stays down. **Check Health Now** in the row menu (or **Check Now** in the flyout) queues one check at once instead of waiting for the schedule. It needs a health check address; without one the request is refused with a message saying so. A monitoring system of your own can write the health it observed through the REST API instead, and the same event fires when it changes. ## Freeze changes [step] A change freeze says that nothing is to be deployed into an environment until a moment you name: the client's month-end, a trade show, a long weekend with nobody on call. 1. Press **Freeze Changes** in the row menu, or **Freeze** in the flyout. 2. Set **Freeze until**, in your own timezone. The moment must be in the future. 3. Press **Freeze**. The row shows a _Frozen_ badge and the flyout shows when the freeze ends; `environment.frozen` goes out with that moment. While it is in force, deployments into the environment are refused and the pipelines are told so. The freeze lifts itself when the moment passes; **Lift Freeze** ends it early and announces `environment.unfrozen`. ## Delete [step] **Delete Environment** removes the environment with its activity history after a confirmation; there is no undo. Deployments and releases that will name environments are not here yet; once they are, an environment with history will be archived rather than deleted. ## What a guest sees A client guest invited to the project sees the Environments page with the **UAT** and **Production** environments only, because those are the two they open: the name, the component, the address, the kind, the health and the flags. The branch, the admin address, the hosting note, the region, the health check address, the failure streak and the notes are not shown, and the guest's own API token leaves those fields out of the payload rather than nulling them. An environment of another kind answers **404** to a guest wherever its address is typed. ## All the fields <Accordions> <Accordion title="Name"> Required, 2 to 60 characters, unique within the project and the component regardless of case. Usually the kind, or the kind and the component. Shown wherever the environment is named, and in the deployment and incident records that will name it. </Accordion> <Accordion title="Kind"> Required. Local, development, QA, staging, UAT, pre-production, production or custom. The kind sets the badge colour, the order in the table and what a guest may see; releases will promote through the kinds in this order. The [status reference](/reference/status-reference#environment-kind) says what each is for. </Accordion> <Accordion title="Component"> Optional, one of the project's components. Leave it empty when every component shares the environment. The name must be unique within the component chosen, so an _API Staging_ and a _Web Staging_ can both exist beside a shared _Staging_. </Accordion> <Accordion title="Address, API Address, Admin Address"> Optional, each a full URL of up to 255 characters. The address is what people open and the one a client guest sees on UAT and production; the API address is what other systems call; the admin address is the back office, staff only. </Accordion> <Accordion title="Health Check Address"> Optional, a full URL. When it is set, the scheduled check calls it; when it is empty, the environment is never checked and its health stays _Not checked_. Staff only. </Accordion> <Accordion title="Branch"> Optional, up to 120 characters. The branch deployed to this environment, such as `main` or `release/1.4`. Staff only. </Accordion> <Accordion title="Current Version"> Optional, up to 60 characters, as last recorded by hand. Deployments will keep it up to date once they land. </Accordion> <Accordion title="Hosting and Region"> Optional, 120 and 60 characters: where the environment runs, in words, and the region it runs in. Staff only. </Accordion> <Accordion title="Auto deploy"> A flag. On when the pipeline deploys to this environment without a hand; the table shows it as a badge. Deployments will read it. </Accordion> <Accordion title="Protected"> A flag. On when a deployment into this environment needs a promotion approval before it runs; production is usually protected. The table shows it as a badge, and deployments will read it. </Accordion> <Accordion title="Notes"> Optional rich text, up to 20,000 characters, staff only: access notes, quirks, who to call. Never a secret. </Accordion> <Accordion title="Health, last checked, failures in a row, frozen until, created and updated"> Read-only. Health and the time it was read are written by the check or by a report over the API; the streak counts checks in a row that found the environment down; the freeze end is set by the freeze dialog; the stamps are kept by the application. All shown in your own timezone. </Accordion> </Accordions> ## What happens next - Every add, edit, freeze, lift, delete and recorded health is written to the environment's [activity history](/workspace/activity-log); the scheduled check is recorded as the application acting, with no person named. - The `environment.created`, `environment.updated`, `environment.deleted`, `environment.frozen`, `environment.unfrozen` and `environment.health_changed` events go out to every endpoint subscribed to them. - A component that environments run cannot be deleted until they are moved or removed. - Deployments, releases and incidents will name environments as those modules land, and the flags, the freeze and the health are what they will read. ## Heimdall on this page Heimdall reads the environments through the same tools the MCP server offers and carries the open project as its context. Try: “Which environments are down right now?” · “What is the staging address and which branch is deployed there?” · “Is production frozen, and until when?” Registering environments, freezing them and recording their health is a person's or a pipeline's work: Heimdall and an agent over MCP read the record and never write to it. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ------------------------------- | -------------------------------------------------------------- | | See the environments | `environment:view-any`; a guest sees UAT and production only | | Open an environment | `environment:view` | | Add an environment | `environment:create`, or owning the team | | Edit, or check health now | `environment:update`, through a project role or a team role | | Freeze, or lift a freeze | `environment:freeze` | | Delete an environment | `environment:delete` | ## Frequently asked <Accordions> <Accordion title="Health says not checked"> The environment has no health check address, or it was saved less than a few minutes ago and the first check has not run yet. Add the address, or press **Check Health Now**. </Accordion> <Accordion title="Health says degraded although the site works"> The check reads anything but a success status as degraded: a redirect to a sign-in page, a 404 on the path given, a 401 from an address that wants credentials. Point the check at an address that answers 200 without signing in, such as the application's own health endpoint. </Accordion> <Accordion title="The freeze dialog refuses my date"> The end of a freeze must be in the future, in your own timezone. A freeze that should have started already still needs an end that is ahead of now. </Accordion> <Accordion title="A guest cannot see staging"> By design. A client guest is shown the UAT and production environments, the two they open; everything else is internal. </Accordion> </Accordions> ## Related - [Components](/projects/components) — the applications environments run - [The project overview](/projects/overview) — the project the environments belong to - [Status reference](/reference/status-reference#environment-kind) — every kind and health - [`environment.*` webhook events](/webhooks/v1/events/environment) --- # Glossary Source: https://alm-docs.labs.zuselab.dev/projects/glossary By the end of this page you will have written down the words a project agrees on, know every field a glossary term carries and what reads each one downstream, and know how a term moves through its review, who sees it at each step and how it is removed. A **glossary term** is a word the project agrees on. The thing the client calls a "branch", the analyst a "site" and the developers a "location" gets one entry with one definition, the other names it goes by, the terms it leans on and where the definition comes from. Every story, requirement and test is read against these words, and Heimdall drafts in them, so the glossary is the project's own language written down once. The Glossary tab is the first of the project's **Dictionary** pages; the second is the [data dictionary](/projects/data-dictionary), the things the project keeps records of. ## Before you begin | You want to | You need | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Open the Glossary tab | `glossary:view-any` on the project. Every staff role has it; a client guest has it through their project membership and sees the approved terms only. | | Add a term | `glossary:create`, through your role on the project or your team role, or to own the team. Business analysts have it by default. | | Edit or move a term | `glossary:update`. | | Delete a term | `glossary:delete`. | The page lives under a project: open the project, choose **Dictionary** in the sidebar and you are on the Glossary tab; the tab strip above the table switches to **Data Entities**. ## Read the glossary [step] The table lists one term per row, in the project's order: | Column | What it shows | | --------------- | ------------------------------------------------------------------------------------------------------------------ | | **Term** | The word, with the first line of its definition beneath it. Press the word to open the detail flyout. | | **Status** | Where the term stands in its review: draft, in review, approved or deprecated, as a coloured badge. Staff only. | | **Other Names** | The first three aliases as tags, and a count of the rest. | | **Related** | How many other terms this one leans on. | | **Source** | Where the definition comes from. | | **Updated** | How long ago the record last changed. | | Row menu | **View Term**, **Edit Term**, **Move to** (the statuses the term may move to), **Activity**, **Delete Term**. | Sort by term, status or updated by pressing a column heading; press it again to flip the direction. Above the table, narrow the list with the search box (words in the term, an alias or the definition) and, for staff, the status filter. The filters travel in the page address; **Clear Filters** on the empty state drops them all. A client guest sees the approved terms only, without the status column or the status filter: a draft is still the analyst's working note. ## Add a term [step] 1. Press **New Term**. 2. Type the **Term**: the word as the project says it, unique within the project. 3. Add the **Source** when the definition comes from somewhere: a document, a person, a standard. 4. Write the **Definition**: what the word means here, in one or two sentences, for the whole project to read. 5. Under **Connections**, list the **Other Names** it goes by, separated by commas, and pick the **Related Terms** it leans on. 6. Press **Add Term**. The term appears in the table at once as a draft and `glossary_term.created` goes out to every endpoint subscribed to it. <Callout type="info" title="The word is all it needs"> A term can start as a bare word; the definition, the other names and the related terms follow as the analyst learns the domain, and Heimdall can draft them from the brief for review. The one rule is uniqueness: two entries for the same word, however they are spelt, would make every story that uses it ambiguous. </Callout> ## Open a term [step] Press a term's word, or **View Term** in its row menu, to open the detail flyout: the status badge, the word and its source, the definition, the **Other Names** as tags, the **Related Terms** as buttons that open the term they name, and the created and updated stamps. From the flyout, **Edit**, **Move to**, **Activity** and **Delete** are one press away. A link such as `/projects/<project>/dictionary?show=<id>` opens the tab with that term's flyout already open; notifications and Heimdall's buttons use it. ## Edit a term [step] 1. Press **Edit Term** in the row menu, or **Edit** in the flyout. 2. Change what you need. A new word must be free within the project, and a term cannot be related to itself. 3. Press **Save Changes**. Every change travels as `glossary_term.updated` with its old and new values, the aliases and the related ids as lists. An edit that changes nothing announces nothing. The status does not change here; it moves from the list or the flyout. ## Move a term along its review [step] A term starts as a **draft**, the analyst's working note. Put it **in review** when the people who approve it should read it, mark it **approved** once they agree, and **deprecate** it when the project stops using the word. Open **Move to** in the row menu or the flyout and choose the status; a deprecated term offers only the way back to draft. The move is written to the term's history and travels as `glossary_term.status_changed` with the status before and after. A client guest sees a term while it is approved and not otherwise, so approving a term is also what shares it with the client. ## Delete a term [step] **Delete Term** in the row menu or **Delete** in the flyout removes the term with its definition and its activity history after a confirmation; there is no undo. The terms that leaned on it drop the relation. Requirements and business rules that will name terms are not here yet; once they are, a term they name will be deprecated rather than deleted. ## All the fields <Accordions> <Accordion title="Term"> Required, 2 to 120 characters, unique within the project regardless of case. The word as the project says it; every story, requirement and test is read against it. </Accordion> <Accordion title="Definition"> Optional rich text, up to 20,000 characters, written for the whole project: the client, the developers and the testers read the same sentence. Heimdall reads it when it drafts in the project's words. </Accordion> <Accordion title="Other Names"> Optional, up to ten names of up to 80 characters each, typed separated by commas and kept as tags. Duplicates are folded regardless of case. The search box finds a term by any of them. </Accordion> <Accordion title="Related Terms"> Optional, up to twenty terms of the same project, never the term itself. Picked from a searchable list; shown in the flyout as buttons that open the term they name, and dropped automatically when a related term is deleted. </Accordion> <Accordion title="Source"> Optional, up to 160 characters. Where the definition comes from: a document, a person, a standard. Shown beneath the word in the flyout. </Accordion> <Accordion title="Status"> _Draft_, _In review_, _Approved_ or _Deprecated_; a new term starts as a draft. Moved from the list or the flyout, never from the editor. The [status reference](/reference/status-reference#artefact-status) says what each step means and which moves are allowed. </Accordion> <Accordion title="Created and updated"> Read-only stamps kept by the application and shown in your own timezone. </Accordion> </Accordions> ## What happens next - Every add, edit, move and delete is written to the term's [activity history](/workspace/activity-log) with who did it and where from. - The `glossary_term.created`, `glossary_term.updated`, `glossary_term.status_changed` and `glossary_term.deleted` events go out to every endpoint subscribed to them. - Requirements, business rules and stories will name terms as those modules land, which is why a term in use is deprecated rather than deleted. ## Heimdall on this page Heimdall reads the glossary through the same tools the MCP server offers and carries the open project as its context. On the editor, **Ask Heimdall** beside each long field drafts it, improves it, shortens it, makes it testable or rewrites it as Given, When, Then; the draft waits under the field until you take it or drop it. See [inline assists](/copilot/inline-assist). Try: “What does the project mean by a branch?” · “Which terms are still in draft?” · “Draft definitions for the terms the brief uses but the glossary lacks.” Writing the glossary is a person's work, with Heimdall drafting for review: an agent over MCP reads the words and never writes to them. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ------------------------------- | ------------------------------------------------------------ | | See the glossary | `glossary:view-any`; a client guest sees the approved terms | | Open a term | `glossary:view` | | Add a term | `glossary:create`, or owning the team | | Edit or move a term | `glossary:update`, through a project role or a team role | | Delete a term | `glossary:delete` | | Read or restore its versions | `glossary:view` to read, `glossary:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="How many terms does a project need?"> Every word two people on the project could read differently. A glossary of twenty sharp terms serves better than two hundred that restate the dictionary; add a word when a story or a meeting shows it means different things to different people. </Accordion> <Accordion title="Can a guest see the glossary?"> Yes, the approved terms. The client reads the words their project is written in and can say where a definition is wrong; drafts and terms in review stay with the team until they are approved. </Accordion> <Accordion title="Should I delete a term the project stopped using?"> Deprecate it. It stays readable with its history and the terms that relate to it, and it says to everyone that the word is no longer the project's; delete only a term that was never right. </Accordion> </Accordions> ## Related - [Traceability](/planning/traceability) — the Links panel in the flyout: what this implements, verifies, depends on or relates to, read from either side - [Comments and mentions](/planning/comments-and-mentions) — the Discussion panel in the flyout: questions, answers, mentions, followers - [Attachments](/planning/attachments) — the Files panel in the flyout: wireframes, specifications and samples pinned on the record - [Data dictionary](/projects/data-dictionary) — the things the project keeps records of - [The project overview](/projects/overview) — the project the glossary belongs to - [Status reference](/reference/status-reference#artefact-status) — the review lifecycle - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [`glossary_term.*` webhook events](/webhooks/v1/events/glossary-term) --- # Projects Source: https://alm-docs.labs.zuselab.dev/projects A project is one client engagement from discovery to maintenance: the container every component, environment, epic, story, requirement, test, release and incident lives under. The registry is where projects are created and shaped, the overview is each project's home once it is open, and the clients page is where the organisations they are done for are kept. Everything downstream reads this record, so these pages come first. ## The pieces | Piece | What it is | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Project** | One engagement, with a key such as `CRM` that prefixes every reference inside it (`CRM-142`), a lifecycle status, a methodology and four leads. | | **Client** | The organisation a project is done for. A project names its client, so the client comes first; a client with projects is archived, never deleted. | | **Key** | Two to six upper-case letters and digits, unique across everything the team ever had. It freezes the moment an artefact is numbered under the project. | | **Status** | Where the project is in its lifecycle: discovery, planning, in development, in testing, live, maintenance, on hold or archived. | | **Health** | On track, at risk, off track or not assessed, computed from the record as the modules that feed it land. | | **Visibility** | Whole team, or members only. A client guest always sees a project through a membership row, whatever its visibility. | | **Component** | One application the project is made of: the web app, the API, a mobile app, a service, with a slug such as `web` that its versions carry, a kind, a tech stack and an owner. | | **Environment** | One place the project runs, from a developer's machine to production: its kind, the component it runs, its addresses, the branch deployed there, the health the last check found and whether a change freeze is in force. | | **Stakeholder** | A person the project answers to, with or without an account: the client's sponsor, product owner and approvers, Zuse's own leads, third parties, each with a side, a role, a weight and the approvals that name them. | | **Persona** | An archetypal user the project is built for, sketched with goals, frustrations and habits: the "as a …" of every story and the actor of every use case. | | **Glossary term** | A word the project agrees on, with one definition, the other names it goes by, the terms it leans on and a review status; the project's own language, read by every story, requirement and test. | | **Data entity** | A thing the project keeps records of, with its fields (type, flags, validation, example), its component, its owner, whether it holds personal data and how long it is kept; the data dictionary the contracts and designs are read against. | | **Design reference** | A link the project keeps to one of its designs: a Figma file, a wireframe, a prototype, a screenshot, an API contract, a diagram or a document, attached to the whole project or to one component, with its version and the notes that say what to look at. | | **Repository** | A Git repository the project's code lives in: its provider, owner and name, the branch releases are cut from, whether it is private and the component it builds; registered by hand now, connected to Git hosting later, and never shown to a client guest. | | **Setup** | The intake wizard: the record built step by step from a client, a name and a paragraph, with presets for the house components and the standard environments, resumable from the sidebar until it is finished. | | **Settings** | The project's knobs: sprint length and working days, timezone, estimation unit and scale, workflow flags, whether guests comment and Heimdall assists, the Telegram group, business hours. Only the keys somebody changed are stored; the rest follow the workspace defaults. | <Callout type="info" title="A paragraph is enough"> A project needs a client, a name and a key to exist, and a description to be useful. From that paragraph Heimdall can draft the components, environments, personas, glossary, entities and epics as the intake proceeds, and the registry grows into the record every later phase reads. </Callout> ## Pages <Cards> </Cards> ## Who may do what | Action | Permission | | ------------------------------- | ------------------------------------------------------------ | | See the registry | `project:view-any`, which every default role carries | | Create a project | `project:create`, or owning the team | | Edit, move or archive a project | `project:update`, through a project role or the team role | | Delete a project | `project:delete` | | See, add, change or delete components | `component:view-any`, `component:create`, `component:update`, `component:delete` | | See, add, change or delete environments | `environment:view-any`, `environment:create`, `environment:update`, `environment:delete`; a guest sees UAT and production only | | Freeze an environment | `environment:freeze` | | See, add, change or remove stakeholders | `stakeholder:view-any`, `stakeholder:create`, `stakeholder:update`, `stakeholder:delete`; a guest sees them without contact details, ratings and notes | | See, add, change or delete personas | `persona:view-any`, `persona:create`, `persona:update`, `persona:delete`; a guest sees them | | See, add, change, move or delete glossary terms | `glossary:view-any`, `glossary:create`, `glossary:update`, `glossary:delete`; a guest sees the approved terms | | See, add, change, move or delete data entities | `data-entity:view-any`, `data-entity:create`, `data-entity:update`, `data-entity:delete`; a guest sees the approved entities | | See, add, change or delete design references | `design-reference:view-any`, `design-reference:create`, `design-reference:update`, `design-reference:delete`; a guest sees them | | See, register, change, disconnect or delete repositories | `repository:view-any`, `repository:create`, `repository:update`, `repository:delete`; a guest is refused | | Open and save the project settings | `project:update`; reading them over the API takes `project:view`, staff only | | See the clients | `client:view-any`; a guest sees the clients of their projects | | Add or change a client | `client:create`, `client:update` | | Delete a client | `client:delete`, and no project may name it | ## Related - [Status reference](/reference/status-reference) — every project, health and client status - [Members and guests](/workspace/members-and-guests) — inviting a client's stakeholder as a guest of one project - [Projects API](/api/v1/reference/projects), [Clients API](/api/v1/reference/clients), [Components API](/api/v1/reference/components), [Environments API](/api/v1/reference/environments), [Stakeholders API](/api/v1/reference/stakeholders), [Personas API](/api/v1/reference/personas), [Glossary API](/api/v1/reference/glossary), [Data Entities API](/api/v1/reference/data-entities), [Design References API](/api/v1/reference/design-references), [Repositories API](/api/v1/reference/repositories) and [Project Settings API](/api/v1/reference/project-settings) - [`project.*`](/webhooks/v1/events/project), [`client.*`](/webhooks/v1/events/client), [`component.*`](/webhooks/v1/events/component), [`environment.*`](/webhooks/v1/events/environment), [`stakeholder.*`](/webhooks/v1/events/stakeholder), [`persona.*`](/webhooks/v1/events/persona), [`glossary_term.*`](/webhooks/v1/events/glossary-term), [`data_entity.*`](/webhooks/v1/events/data-entity), [`design_reference.*`](/webhooks/v1/events/design-reference) and [`repository.*`](/webhooks/v1/events/repository) webhook events --- # The Intake Wizard Source: https://alm-docs.labs.zuselab.dev/projects/intake-wizard By the end of this page you will have started a project from a client, a name and a paragraph, walked its record through the components, environments, stakeholders and personas it needs, and know how to leave the wizard half-way and pick it up again from any device. The **intake wizard** is how a project is born in Zuse Bifrost. It asks for as little as it can up front, writes every row it collects into the module's own table through the module's own rules, and remembers which steps you saved or skipped, so the record grows in the order the later phases read it: what the project is, what it is made of, where it runs, who it answers to and who it is built for. Nothing you add in the wizard is special: a component added here is the same component the [Components](/projects/components) page shows, and it can be edited there afterwards. ## Before you begin | You want to | You need | | ------------------------------ | -------------------------------------------------------------------------------------------------------- | | Start a project | `project:create` on your team role, or to own the team. Admins and project managers have it by default. | | Resume a project's setup | `project:update` on the project, through your role on it or your team role. A client guest never has it. | | Add components or environments | `component:create` and `environment:create` on the project. | | Add stakeholders or personas | `stakeholder:create` and `persona:create` on the project. | | Name a client on the project | The client to exist already; add it on the [Clients](/projects/clients) page first. | The wizard has two doors. **New Project** on the [registry](/projects/registry) opens it empty, to start a project. **Setup** in a project's sidebar opens it on that project, at the first step not yet saved; the entry stays in the sidebar, with a count of the steps saved, until the setup is finished. The project overview carries the same reminder. ## Start a project [step] 1. Press **New Project** on the registry. The wizard opens on the **Basics** step with nothing filled in. 2. Type the **Name** and pick the **Client**, or leave _Internal project_ for work the team does for itself. 3. Write the **Description**: one paragraph on what is being built, for whom and why. This is the brief Heimdall reads when it drafts the rest of the record, so a paragraph now saves an afternoon later. 4. Leave the **Key** empty and one is proposed from the name: three or more words give their initials (_Customer Relationship Management_ becomes `CRM`), fewer give the first three letters (_Loyalty Platform_ becomes `LOY`), and a key the team ever used gets a digit appended. Type your own if you prefer; it is upper-cased as you go. 5. Set how it runs, name the leads you know and the dates agreed, or leave them for later; the same fields as the registry's editor are here, and every one can change afterwards. 6. Press **Create Project**. The project is created, `project.created` goes out, and the wizard moves on to the **Components** step of the new project. From here on you are resuming, and everything on this page applies. <Callout type="info" title="A paragraph is enough"> A client, a name and a key are what a project needs to exist. The paragraph is what makes the rest of the setup fast: it is the brief every later draft is read from, and the wizard's presets already assume the house stack unless it says otherwise. </Callout> ## Walk the steps [step] The steps run across the top of the page: **Basics**, **Components**, **Environments**, **Stakeholders**, **Personas** and **Review**. Each shows its state: the step you are on, a step saved (a tick), a step skipped for now (a dashed outline) and a step not reached yet. Press any step to open it, in any order. Under every step except the review, the footer offers: - **Continue** marks the step as saved and opens the next one. For the basics, the button is **Save and Continue** and writes the fields first. - **Skip for now** leaves the step open and moves on. A skipped step keeps counting as unfinished in the sidebar and the overview until it is saved. - **Back** returns to the previous step. Every row you add is written the moment you add it, so there is nothing to lose by leaving: close the tab, open the project on another device, and **Setup** in the sidebar brings you back to the first step still open. The step you were on also travels in the page address, so a link to a step can be shared with the colleague who owns it. ## Components [step] What the project is made of; stories and requirements tag the components they touch, and every release carries a version per component. 1. Press **Add the house set** for a **Web App** and an **API** on the house stack, the Laravel stack the team builds on. Each is added once; pressing again when both exist changes nothing. 2. To add a component by hand, type its **Name**, pick its **Kind** and adjust the **Tech Stack**, filled in with the house stack for a web app, an API or a service, then press **Add Component**. The slug its versions carry is derived from the name. 3. Press the bin on a row to remove a component added by mistake. A component an environment already runs is refused; move the environment or archive the component on its page instead. Everything else about a component, its owner, its description, archiving, lives on the [Components](/projects/components) page. ## Environments [step] Where the project runs; environments feed releases, deployments and health checks, and hold the addresses the client is shown. 1. Press **Add the standard set** for **Development**, **Staging**, **UAT** and **Production**, shared across components, with production marked _protected_ so a deployment into it will need a promotion approval once deployments land. Each is added once. 2. To add one by hand, type its **Name**, pick its **Kind** and the **Component** it runs (or leave it shared), add its **Address** if it exists, and press **Add Environment**. 3. Press the bin on a row to remove an environment; nothing else is removed with it. The branch, the hosting, the health check and the flags live on the [Environments](/projects/environments) page. ## Stakeholders [step] Who the project answers to; the approver flags decide who signs off requirements, UAT and releases, and the weights shape the status reports. 1. Type the **Name** and **Title**, pick the **Side** (client, Zuse or a third party) and the **Role**, and add the **Email** if you have it; it is needed before the person can be invited as a guest. 2. Switch on **Approves requirements**, **Signs off UAT** and **Approves releases** as they apply. These are the flags the approval policies read when they name their approvers. 3. Press **Add Stakeholder**. Press the bin on a row to remove one. Contact details, the influence and interest weights, the RACI default and the linked account are on the [Stakeholders](/projects/stakeholders) page. ## Personas [step] Who the project is built for; a persona is the subject of every story and the actor of every use case. 1. Type the **Name** and the **Role Title** in the words the client uses, a one-line **Tagline**, and the **Goals** the persona is trying to reach. 2. Press **Add Persona**. Press the bin on a row to remove one. The full sketch, demographics, frustrations, habits, devices and a quote, is written on the [Personas](/projects/personas) page. ## Review and finish [step] The **Review** step shows one card per step: the project's key, client and methodology for the basics, and the names and the count for the rest, each marked saved, skipped or untouched. On a card, **Revisit** or **Fill in** opens the step, and **Open the page** leaves for the module's own page. Press **Finish Setup** when the record is as complete as you need it today. The wizard closes, **Setup** leaves the sidebar, the overview's reminder goes, and you land on the [project overview](/projects/overview). Finishing freezes nothing: every row stays editable on its page, and the wizard itself can be reopened from its address at any time. ## What happens next - Every row the wizard writes is announced the way its page would announce it: `project.created` for the project, then `component.created`, `environment.created`, `stakeholder.created` and `persona.created` for each row, and the matching `*.deleted` for a row removed. The progress itself is bookkeeping and goes out to nobody. - Every write is in the project's [activity history](/workspace/activity-log) with who did it and where from. - The glossary, the data entities, the designs, the repositories and the settings have pages of their own under the project; the planning record follows them. ## Heimdall on this page The fastest path from the client's words to a planning record is the [brief import](/planning/brief-import): paste the brief on the project's **Brief Import** page once the wizard has made the project, and Heimdall drafts the epics, stories, requirements, rules, terms, personas, stakeholders, risks and decisions for you to accept from a review queue. Heimdall reads the record the wizard builds through the same tools the MCP server offers, and the paragraph you wrote in the basics is the brief it drafts from. Try: “Which projects still have their setup open?” · “What does this project run on, and where?” · “Who signs off UAT on this project?” ## From the API and MCP The wizard has no API of its own: an integration creates the project and its rows through the module endpoints, and the project then shows **Setup** in the sidebar until a person finishes it. <Cards> </Cards> ## Permissions | Action | Permission | | ------------------------------- | --------------------------------------------- | | Start a project | `project:create` on the team | | Resume, skip, review and finish | `project:update` on the project | | Save the basics again | `project:update` | | Add or remove components | `component:create` and `component:delete` | | Add or remove environments | `environment:create` and `environment:delete` | | Add or remove stakeholders | `stakeholder:create` and `stakeholder:delete` | | Add or remove personas | `persona:create` and `persona:delete` | A client guest never opens the wizard and never sees the sidebar entry or the overview's reminder. ## Frequently asked <Accordions> <Accordion title="Can I reopen a finished setup?"> Yes. Open `/projects/<id>/setup` and the wizard shows the review with every step; nothing is locked by finishing. The sidebar entry does not come back, because the reminder has done its job. </Accordion> <Accordion title="What if the proposed key is wrong?"> Type the one you want before pressing Create Project, or change it afterwards in the registry's editor; a key stays changeable until an artefact is numbered under the project. </Accordion> <Accordion title="Does skipping a step lose anything?"> No. Skipping only moves on; the step stays open and keeps counting until it is saved, and anything already added on it stays. </Accordion> <Accordion title="Why does a project created over the API show the Setup entry?"> Because its wizard was never finished. Open it, review what the integration created, and press Finish Setup; or add what is missing first. </Accordion> </Accordions> ## Related - [Brief import](/planning/brief-import) — the scope, the words and the risks drafted from the client's brief - [The registry](/projects/registry) — where the New Project button lives and the project is edited afterwards - [The project overview](/projects/overview) — the reminder to finish the setup, and where finishing lands - [Components](/projects/components), [Environments](/projects/environments), [Stakeholders](/projects/stakeholders) and [Personas](/projects/personas) — the pages every wizard row belongs to - [Project settings](/projects/project-settings) — the knobs the wizard does not touch --- # The Project Overview Source: https://alm-docs.labs.zuselab.dev/projects/overview By the end of this page you will know the three ways into a project, what the overview tells you at a glance, how to have Heimdall research the project's domain from the rail under the lifecycle, how the sidebar changes while a project is open, and which parts of the page a client guest is shown. A project's overview is its home: the record the registry holds, laid out to be read rather than edited, with the project's place in its lifecycle drawn across the top and the latest changes under it at the bottom. Every page that belongs to the project (its components, environments, people, planning record, board, tests, releases and incidents, as those modules arrive) hangs off the same sidebar menu that appears when the overview opens. ## Before you begin | You want to | You need | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Open a project | To be able to see it: a staff member sees every project of their team shared with the whole team, plus the members-only projects they were added to; a guest sees the projects they were invited to. | | Change what the overview shows | `project:update` on the project, through the registry's editor. The overview itself writes nothing. | A project that is hidden from you answers **404** wherever its address is typed, so a project you are not allowed to see looks like one that does not exist. ## Open a project [step] Three doors lead to the overview, and they all end on the same address, `/projects/<id>`: - **The project selector** at the top of the sidebar lists every live project you may see, each with its initials, its name and its key, and a dot in the colour of its health. Type to search; choose one and the overview opens. Clear the selector to return to the dashboard. Archived projects are not offered here; reach them from the registry. - **The registry** opens a project with **Open Project** in a row's menu, or with the **Open Project** button at the foot of a project's detail flyout. - **Heimdall** answers a question about a project with a button named after it ("View Customer Portal") that opens the overview; a question about several projects gets **Open the projects** instead, which opens the registry. The selector refreshes itself when a project is created, renamed, archived or deleted elsewhere, so a project a colleague added a minute ago is in the list without a reload. ## Read the overview [step] The page opens with the project's name and tagline. Under them: - **The badges**: the key (`CRM`), the lifecycle status, the health, the methodology and, for staff, the visibility (whole team or members only). - **The lifecycle strip**: the six steps a project passes through, _Discovery_, _Planning_, _In development_, _In testing_, _Live_ and _Maintenance_, with the steps behind the project ticked, the current one highlighted and the rest ahead. A project _On hold_ or _Archived_ stands on no step; its status badge says so. - **Heimdall suggests** (staff only): the rail where Heimdall researches the project's domain and leaves notes and suggestions; the [next step](#ask-heimdall-about-the-domain) walks through it. - **Client**: the organisation the project is done for, with its status and industry, and for staff the primary contact. An internal project says so instead. - **Dates**: when the project starts, when it is due to launch and when it launched. - **Leads** (staff only): the project manager, business analyst, lead developer and QA lead, or _Unassigned_ where nobody holds the role yet. - **Budget** (staff only): the hours agreed with the client, which time logs and timesheets report against once work is tracked, or _No budget agreed_. - **Description**: the paragraph the registry holds, rendered as written. - **Recent Activity** (staff only): the newest changes recorded anywhere in the project, newest first, with who made each and where it came from, and **Load more** for the older ones. **Open in registry** at the top right opens the registry with this project's detail flyout already showing, from where **Edit**, **Move to**, **Archive** and the rest are one press away. The overview itself changes nothing: every write to a project goes through the registry, so the rules and the history there are the only ones. While the project's setup is still open and you may change the project, a reminder above the lifecycle counts the steps saved, and **Continue Setup** opens the [intake wizard](/projects/intake-wizard) at the first step still open. Finishing the wizard takes the reminder away. ## Ask Heimdall about the domain [step] Between the lifecycle strip and the cards, staff who may see the project's imports find the **Heimdall suggests** rail. On a project never researched it explains itself in a sentence and offers **Research the domain**. Press it and Heimdall reads the web for what this kind of product needs: the features clients expect, the laws and standards that apply in the client's country and industry, the integrations the client will assume, the baselines of the market, and the security and accessibility expectations the project will be held to. The ask is answered at once with a toast; the run is worked on the queue as you, the badge beside the heading moves from **Queued** to **Researching**, and the rail refreshes itself every few seconds until the run is **Ready** or **Failed**. One run at a time: a second press while one is open is refused with a toast. When the run is ready the rail shows, top to bottom: - **Check the record** runs Heimdall's [improvement sweep](/copilot/improvement-sweep) now: it reads the record for a requirement without a measure, a story without a negative criterion, a rule without an example, an entity without an owner and a risk without a mitigation, and leaves one card per gap under **Waiting for you**, each with **Open** to the record. It calls no model. - **Who asked and when**, from the newest run, and **Research again** to run it once more. A later run adds to the earlier ones: ideas the project already holds or dismissed are left out of its answer, so pressing it after the record grew is cheap and useful, pressing it on an unchanged project mostly repeats the cost. - **Waiting for you**: a card per suggestion still undecided, two to a row. Each names its kind (**Research** for a fact the project must honour, **Improvement** for a gap in the record), how sure Heimdall is, the title, a summary, the one-line reason and the pages it read, each a link that opens in a new tab. People who may decide imports see **Keep** and **Dismiss** on the card: **Keep** holds the note on the project for everyone to read; **Dismiss** drops it and tells Heimdall never to propose it again. Neither writes a record; a kept note is something to act on through the editors or the [brief import](/planning/brief-import). - **What Heimdall learned**: the notes of the newest finished run, one row per topic, folded; open a row for the paragraphs and the sources. - **Kept**: the suggestions people kept, newest first, with who kept each. A run that failed says why in the rail (the model did not answer, the answer could not be read, the budget had no room, or the person who asked is gone) and offers **Research again**; nothing was written and nothing was charged. A project whose assists are off under Settings → AI shows the rail with what earlier runs left and without the ask. How the researcher thinks, what it is told and what a run costs is on the [Heimdall research](/copilot/research) page. ## The sidebar while a project is open [step] Opening a project swaps the sidebar's menu. The **Selected Project** group starts with **Project Overview**, then **Setup** while the [intake wizard](/projects/intake-wizard) is still open, badged with the steps saved and shown only to people who may change the project, then **[Components](/projects/components)**, **[Environments](/projects/environments)**, **People** (the [stakeholders](/projects/stakeholders) and [personas](/projects/personas) tabs), **Dictionary** (the [glossary](/projects/glossary) and [data entities](/projects/data-dictionary) tabs), **[Designs](/projects/design-references)** and **Code** (the [repositories](/projects/repositories) tab), each badged with how many you may see (a guest is shown the environments, the people, the approved dictionary and the designs, and neither the components nor the code), then **[Settings](/projects/project-settings)** for the people who may change the project, and each later module adds its own entry as it lands: the planning record, the board, tests, releases and maintenance. Above the notifications bell, **Back to Dashboard** returns to the overview of your work across every project; the selector stays where it is, showing the project you are in, and still switches to another in one choice. Without a project the menu shows **Dashboard**, **Projects** (badged with the number of live projects you may see) and **Clients** (badged with your team's live clients). ## What a guest sees A client guest invited to the project sees the same page without its internal parts: the badges except the visibility, the lifecycle strip, the client's name, status and industry without the contact, the dates and the description. The leads, the budget, the activity and the Heimdall suggests rail are not shown, and the selector and menu offer only the projects the guest was invited to. ## What happens next The overview itself writes nothing: it reads the same row the registry writes, so a status moved or a lead assigned there shows here on the next visit, and the activity card records it as soon as it happens. The Heimdall suggests rail is the exception. Asking for research emits `research.started`, and the queue follows with `research.ready` (the counts of notes and suggestions) or `research.failed` (the reason). Keeping a suggestion emits `import.suggestion_accepted` and dismissing one `import.suggestion_rejected`, the same events a [brief import's](/planning/brief-import) decisions emit, with no batch named. Nobody is notified of any of them; the rail is where the answer lands. ## Heimdall on this page Heimdall panel carries the open project as its context, so a question needs no key: - "Where does this project stand?" - "Who leads this project and when is it due?" - "Summarise what changed on this project this week." The answers come from `get_project` and `get_activity_log`, and the button under an answer opens the page it read. ## From the API and MCP <Cards> </Cards> The research runs have no MCP tool yet: an agent reads the notes through the REST endpoint. ## Permissions | Action | Permission | | ------------------------------------------- | ---------------------------------------------------------------------------------- | | Open the overview | None beyond seeing the project; every default role sees the team's shared projects | | Open a members-only project | A membership row on the project, or owning the team | | See the leads, budget, contact and activity | To be staff; a guest is never shown them | | See the Heimdall suggests rail | `import:view-any`, as staff; a guest is never shown it | | Research the domain, or check the record | `import:create`, with the project's assists on | | Keep or dismiss a suggestion | `import:update` | | Change anything shown | `project:update`, through the registry | ## Frequently asked **Why does the selector not show a project I know exists?** It lists live projects you may see. An archived project is left out on purpose; find it in the registry with the **Archived** switch on, restore it there if it should be worked on again. A project of another team, or a members-only project you were not added to, is not yours to see. **Can I edit the description or move the status here?** Not on this page. Press **Open in registry** and use the flyout's controls; the registry is the one place projects are changed, so its validation and its history stay complete. **The lifecycle shows every step ahead. Is that a bug?** No. A project _On hold_ or _Archived_ is paused rather than at a step; the status badge shows which, and restoring or moving the project puts it back on the path. **I dismissed a suggestion by mistake. Can I get it back?** No. A dismissal is a decision Heimdall is told to honour on every later run, and decisions are never undone. Write what the note proposed yourself, through the record's own editor or the brief import, if it turns out to matter. **Why did the second run leave fewer suggestions than the first?** Because the first run's ideas, kept, pending or dismissed, are left out of every later answer, and so are the titles the record now holds. A second run on an unchanged project is mostly new notes and few new suggestions. **Why is the activity card empty on a new project?** It lists the changes recorded under the project. A project created a moment ago has its creation entry and nothing else until something under it changes. ## Related - [The registry](/projects/registry) — where projects are created, edited, moved, archived and deleted - [Clients](/projects/clients) — the organisations projects are done for - [Status reference](/reference/status-reference) — every status on the lifecycle strip and what moves a project into it - [Activity log](/workspace/activity-log) — the record behind the activity card - [Heimdall panel](/copilot/panel) — the buttons that open a project from an answer - [Heimdall research](/copilot/research) — what the researcher is told, how it answers and what a run costs - [Improvement sweep](/copilot/improvement-sweep) — the five gaps Check the record looks for, and why a card is never proposed twice - [Brief import](/planning/brief-import) — the review queue the kept and dismissed notes share their shape with --- # Personas Source: https://alm-docs.labs.zuselab.dev/projects/personas By the end of this page you will have sketched who a project is built for, know every field a persona carries and what reads each one downstream, and know how a persona is put away, brought back and, when it must be, deleted. A **persona** is an archetypal user: a named, sketched person with goals, frustrations and habits, so the analyst, the developers and the testers picture the same someone. Personas are the "as a …" of every user story and the actors of every use case; Heimdall reads them when it drafts stories and tests, and the test cases will be read against the devices and the proficiency you record here. The Personas tab is the second of the project's **People** pages; the first is [Stakeholders](/projects/stakeholders), who the project answers to. ## Before you begin | You want to | You need | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Open the Personas tab | `persona:view-any` on the project. Every staff role has it; a client guest has it through their project membership. | | Add a persona | `persona:create`, through your role on the project or your team role, or to own the team. Business analysts have it by default. | | Edit, archive or restore one | `persona:update`. | | Delete one | `persona:delete`. | The page lives under a project: open the project, choose **People** in the sidebar and switch to **Personas** in the tab strip above the table. ## Read the personas [step] The table lists one persona per row, in the project's order: | Column | What it shows | | -------------------- | -------------------------------------------------------------------------------------------------------- | | **Persona** | An initials tile, the name and the tagline beneath it. Press the name to open the detail flyout. An archived persona carries an _Archived_ badge. | | **Role** | What they do, in their own world. | | **Tech Proficiency** | How at ease they are with technology: low, medium or high, as a coloured badge. | | **Devices** | The first three devices they use, and a count of the rest. | | **Updated** | How long ago the record last changed. | | Row menu | **View Persona**, **Edit Persona**, **Archive Persona** or **Restore Persona**, **Activity**, **Delete Persona**. | Sort by persona, tech proficiency or updated by pressing a column heading; press it again to flip the direction. Above the table, narrow the list with the search box (words in the name, the tagline or the role title) and the **Archived** switch, which shows archived personas instead of live ones. The filters travel in the page address; **Clear Filters** on the empty state drops them all. ## Add a persona [step] 1. Press **New Persona**. 2. Type the **Name**: a memorable handle such as _Busy Buyer_, not a real person. It is unique within the project. 3. Add the **Role Title** (what they do, in their own world) and a one-line **Tagline**. 4. Write the **Sketch**: who they are under **Demographics**, what they want under **Goals**, what gets in their way under **Frustrations**, and how they work under **Behaviours**. 5. Under **Technology**, rate their **Tech Proficiency** and list the **Devices** they use, separated by commas. 6. Give them a **Quote**: something they would say about the product or their day. 7. Press **Create Persona**. The persona appears in the table at once and `persona.created` goes out to every endpoint subscribed to it. <Callout type="info" title="A name is all it needs to start"> The sketch can grow as the analyst learns the users, and Heimdall can draft the whole sketch from the brief for review. The one rule is the name: stories and use cases will name personas, and two _Busy Buyer_s would make the backlog ambiguous. </Callout> ## Open a persona [step] Press a persona's name, or **View Persona** in its row menu, to open the detail flyout: the initials tile, the proficiency badge, the name, role title and tagline, the quote set apart, the **Devices** as tags, each part of the sketch that was written, and the created and updated stamps. From the flyout, **Edit**, **Archive** or **Restore**, **Activity** and **Delete** are one press away. A link such as `/projects/<project>/people/personas?show=<id>` opens the tab with that persona's flyout already open; notifications and Heimdall's buttons use it. ## Edit a persona [step] 1. Press **Edit Persona** in the row menu, or **Edit** in the flyout. 2. Change what you need. A new name must be free within the project. 3. Press **Save Changes**. Every change travels as `persona.updated` with its old and new values, the devices as a list. An edit that changes nothing announces nothing. ## Archive, restore and delete [step] - **Archive Persona** takes the persona out of the default view. The stories and use cases that will name it keep resolving; switch on **Archived** above the table to find it. - **Restore Persona** brings an archived persona back into the default view. - **Delete Persona** removes the persona with its sketch and its activity history after a confirmation; there is no undo. Stories and use cases that will name personas are not here yet; once they are, a persona they name will be archived rather than deleted. ## All the fields <Accordions> <Accordion title="Name"> Required, 2 to 80 characters, unique within the project regardless of case. The handle every story and use case will name. </Accordion> <Accordion title="Role Title and Tagline"> Optional, 80 and 160 characters. What they do in their own world, and one line that sums them up; both shown beneath the name. </Accordion> <Accordion title="Demographics, Goals, Frustrations, Behaviours"> Optional rich text, up to 20,000 characters each: the sketch. Heimdall reads all four when it drafts stories, acceptance criteria and tests for this persona. </Accordion> <Accordion title="Tech Proficiency"> Required, _Low_, _Medium_ or _High_. How at ease they are with new software; the test cases and the usability requirements read it. The [status reference](/reference/status-reference#influence-interest-and-proficiency) says what each step means. </Accordion> <Accordion title="Devices"> Optional, up to ten devices of up to 40 characters each, typed separated by commas and kept as tags. Duplicates are folded regardless of case. The test plans read them when they choose environments and browsers. </Accordion> <Accordion title="Quote"> Optional, up to 300 characters. Something they would say, set apart in the flyout. </Accordion> <Accordion title="Archived, created and updated"> Read-only stamps kept by the application and shown in your own timezone. </Accordion> </Accordions> ## What happens next - Every add, edit, archive, restore and delete is written to the persona's [activity history](/workspace/activity-log) with who did it and where from. - The `persona.created`, `persona.updated`, `persona.archived`, `persona.restored` and `persona.deleted` events go out to every endpoint subscribed to them. - Stories, use cases and test cases will name personas as those modules land, which is why a persona in use is archived rather than deleted. ## Heimdall on this page Heimdall reads the personas through the same tools the MCP server offers and carries the open project as its context. Try: “Who is this project built for?” · “What frustrates the Busy Buyer today?” · “Draft a persona for the warehouse staff from the brief.” Sketching personas is a person's work, with Heimdall drafting for review: an agent over MCP reads the record and never writes to it. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ------------------------------- | ------------------------------------------------------------ | | See the personas | `persona:view-any`, a client guest included | | Open a persona | `persona:view` | | Add a persona | `persona:create`, or owning the team | | Edit, archive or restore | `persona:update`, through a project role or a team role | | Delete a persona | `persona:delete` | | Read or restore its versions | `persona:view` to read, `persona:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="How many personas does a project need?"> As many as there are kinds of people whose goals differ: usually three to five. A persona per job title is too many; a single "user" is too few to write a story for. </Accordion> <Accordion title="Can a guest see the personas?"> Yes. A persona describes the client's own users, so the client's stakeholders read them and can say where the sketch is wrong. </Accordion> <Accordion title="Should I delete a persona nobody writes stories for?"> Archive it. It leaves the default view, keeps its sketch and its history, and comes back with one press if the users turn out to exist after all. </Accordion> </Accordions> ## Related - [Stakeholders](/projects/stakeholders) — who the project answers to - [The project overview](/projects/overview) — the project the personas belong to - [Status reference](/reference/status-reference#influence-interest-and-proficiency) — the three-step scale - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [`persona.*` webhook events](/webhooks/v1/events/persona) --- # Project Settings Source: https://alm-docs.labs.zuselab.dev/projects/project-settings By the end of this page you will have tuned a project's knobs once so every later module reads them rather than asking again, know every setting with its default and what reads it, and know how a project is archived, restored and deleted from its own settings. A project's **settings** are the few things that differ from project to project without being part of its record: how long a sprint runs and on which days, which timezone its deadlines are read in, what work is sized in, what moves an item along, whether the client may comment, whether Heimdall assists, and where the project speaks when something happens. A project stores only the keys somebody changed; everything else is the workspace default. ## Before you begin | You want to | You need | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Open the Settings page | `project:update` on the project, through your role on the project or your team role, or to own the team. Project managers have it; a client guest never does. | | Delete the project | `project:delete`. | | Change the approval policies | `approval:manage-policies`, through a project role or a team role. | The page lives under a project: open the project and choose **Settings** in the sidebar. The entry appears only to people who may change the project. ## Set the cadence [step] 1. Pick the **Timezone** sprint boundaries, deadlines and SLAs are read in, or leave it on the workspace default. 2. Set the **Sprint Length** in days. 3. Tick the **Working Days**; capacity, burndowns and SLAs skip the rest. 4. Set when the **Business Day Starts** and **Ends**; incident and ticket SLAs count between them. 5. Choose the **Estimation Unit** and, for points, the **Story Point Scale**. ## Set the workflow [step] 1. Choose the **Default Assignee** a new item goes to when nobody is named. 2. Switch on or off whether an opened pull request moves an item to review, whether a merged one moves it on, whether a story needs its Definition of Ready before a sprint takes it, and whether an item needs its Definition of Done before it is done. These take effect as the board, the sprints and the Git integration land; setting them now means those modules start the way the project wants them. ## Set collaboration and alerts [step] 1. Decide whether **Guests may comment** and whether **Heimdall assists on this project**. 2. Paste the **Telegram Group Chat** id that should receive the project's alerts, or leave it empty. 3. Decide whether a production environment found down twice should **open an incident** once the maintenance module lands. Press **Save Settings**. Only the keys that differ from the workspace defaults are stored, every module reads the merged result, and `project.settings_changed` goes out with the keys that changed. **Restore Defaults** puts the workspace defaults back on the form without saving them; press **Save Settings** to keep them. <Callout type="info" title="Defaults are the workspace's, not the project's"> A key you never touched follows the workspace default, so a change to the default later reaches every project that never overrode it. A key you set stays as you set it. </Callout> ## Seat the council [step] Under **The Council**, tick the voices of [Heimdall's council](/copilot/council) that sit on each open moment of this project (the **Intake scaffold** on a brief import, the **Requirement review** on a story, requirement or rule), each voice shown with its lens, and set the **Council Budget Share**: the percentage of the team's monthly AI budget the council may spend on this project, 1 to 100. Every voice of a moment sits by default; a moment with no voice ticked cannot be convened, and a voice left unticked is refused when asked for alone. A team without a cap is never refused on the share. Press **Save Settings** as for the rest. ## Set the approval policies [step] Under **Approvals**, one editor per open kind of [approval](/planning/approvals) says who decides it on this project, how many must agree and how long they have: the **Rule** and the **Quorum**, the **Approver roles**, **Named people** and **Stakeholder seats** asked, the **Hours to decide**, the **Reminder after**, the hours after which a request carries by itself, whether a refusal needs a reason, and whether the gate is on at all. A kind nobody changed reads the workspace defaults and says so with a **Workspace default** badge; **Save Policy** on an editor writes that kind alone and turns the badge into **Set on this project**, and requests already open keep the approvers they were sent to. The **Requirement review**, the **Baseline approval**, the **Client sign-off** and the **Change request** are open today; the client sign-off takes its approvers from the stakeholder seats flagged as approving requirements rather than from the policy's lists. The editors need `approval:manage-policies`; everyone else who may open the page reads a summary. Every field is explained on the [approvals page](/planning/approvals#set-the-policies). ## The danger zone [step] At the foot of the page: - **Archive Project** takes the project out of the registry's default view and keeps everything under it; **Restore Project** brings it back. - **Delete Project** removes the project with every component, environment, person, term, entity, design and repository under it. Type the project key to confirm; there is no undo. Only a person with `project:delete` sees it. ## All the settings <Accordions> <Accordion title="Sprint length (sprint_length_days)"> 1 to 60 days, 14 by default. The sprint planner proposes end dates from it and velocity is read per sprint of this length. </Accordion> <Accordion title="Working days (working_days)"> The days of the week work happens, 1 for Monday to 7 for Sunday, Monday to Friday by default; at least one. Capacity, burndowns and SLAs skip the rest. </Accordion> <Accordion title="Timezone (timezone)"> A timezone identifier such as `Asia/Colombo`, or none for the workspace default. Sprint boundaries, deadlines, maintenance windows and SLAs are read in it; what you see on screen stays in your own timezone. </Accordion> <Accordion title="Estimation unit and story point scale (estimation_unit, story_point_scale)"> Work is sized in _Story points_ or _Hours_; points by default. For points, the scale is _Fibonacci_ (1, 2, 3, 5, 8, 13, 21), _Linear_ (1 to 10) or _T-shirt sizes_ (XS to XL), Fibonacci by default. The estimate picker offers the steps and the sprint planner sums them. </Accordion> <Accordion title="Default assignee (default_assignee_rule)"> Who a new item goes to when nobody is named: the person who raised it (the default), the project's lead developer, or nobody. </Accordion> <Accordion title="Pull request transitions (auto_transition_on_pr, auto_transition_on_merge)"> Both on by default. With the first, an opened pull request that names an item moves it to In Review; with the second, a merged one moves a story to QA and a task to Done. The Git integration reads them. </Accordion> <Accordion title="Definition of Ready and Done (require_dor_for_sprint, require_dod_for_done)"> Off and on by default. With the first, a story joins a sprint only once its readiness checklist is ticked; with the second, an item becomes Done only once its done checklist is ticked. The board and the sprints read them. </Accordion> <Accordion title="Guests may comment (guest_can_comment)"> On by default. Client guests can leave [comments](/planning/comments-and-mentions) on what they may read; internal comments stay internal whatever the switch says. Off, a guest reads the open comments and posts nothing. </Accordion> <Accordion title="Heimdall assists (ai_assist_enabled)"> On by default. Drafts, reviews and suggestions are offered on the project's pages; off, Heimdall only answers questions and never proposes a change. </Accordion> <Accordion title="Telegram group (telegram_group_chat_id)"> The chat id of the group that receives the project's alerts, a number with a leading minus for a group; none by default. Personal alerts still reach each person's own linked chat. </Accordion> <Accordion title="Board limits and hidden columns (wip_limits, hidden_columns)"> The most items an open status may hold, by status code, and the status codes the [board](/building/board) does not draw; both empty by default. A limit counts the whole project's items in that status, and once the column holds its limit every move into it is refused, on the board, on the backlog, from an agent and over the API, until something leaves. The closed statuses, done and cancelled, take no limit. A hidden column keeps its items in the backlog and in its counts; they are simply not drawn on the board, and a card's **Move to** there does not offer a hidden column. Both are read and written through the project settings endpoint and tool today; the Workflow tab of this page edits them when it lands. </Accordion> <Accordion title="Business hours (business_hours)"> When the business day starts and ends, as `HH:MM` in the project's timezone, 09:00 to 18:00 by default; the end must be after the start. Incident and ticket SLAs count between them. </Accordion> <Accordion title="Open an incident when production is down (auto_incident_on_down)"> On by default. A production environment found down twice in a row opens an incident once the maintenance module lands; the [environment health checks](/projects/environments) already count the failures. </Accordion> <Accordion title="Council voices (council_voices)"> The voices of [Heimdall's council](/copilot/council) that sit on each moment, keyed by moment; every voice the moment seats by default. Odin reads for architecture, Thor for testing, Loki for security, Týr for rigour and compliance, Frigg for planning, Baldur for the people who will use the product and Hel for what fails in production. A moment left with no voice cannot be convened. Over the API a moment left out of the map keeps its default seats. </Accordion> <Accordion title="Council budget share (council_budget_percent)"> The share of the team's monthly AI budget the council may spend on this project, as a percentage from 1 to 100, 25 by default. A voice that would push the month's council spend past it fails with the reason while the other voices go on; a team without a cap is never refused. </Accordion> </Accordions> ## What happens next - Every save is written to the project's [activity history](/workspace/activity-log) with who did it and where from, and `project.settings_changed` goes out with the keys that changed. - Every module that lands reads the settings from here: the sprint planner, the board, the Git integration, the SLA timers, the alert routing and Heimdall's assists. ## Heimdall on this page Heimdall reads the settings through the same tool the MCP server offers and carries the open project as its context. Try: “How long are this project's sprints, and which days count?” · “Is this project sized in points or hours?” · “Which projects have Heimdall's assists switched off?” Tuning a project is a person's work: an agent over MCP reads the settings to plan and size the way the project does, and never changes them. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ------------------------------ | ------------------------------------------------------- | | Open and save the settings | `project:update`, through a project role or a team role | | Read the settings over the API | `project:view`, staff only; a client guest is refused | | Archive or restore | `project:update` | | Delete the project | `project:delete` | | Change the approval policies | `approval:manage-policies` | ## Frequently asked <Accordions> <Accordion title="Why can a client guest read the project but not its settings?"> The settings are how the team works, not what the client is buying: a sprint length, a Telegram group, whether Heimdall assists. The client reads the record the settings produce. </Accordion> <Accordion title="What happens to the board flags before the board exists?"> They are stored and read by every module as it lands, so a project set up today starts the board, the sprints and the Git integration the way it wants them. </Accordion> <Accordion title="Can I change a setting from the API?"> Yes, with `project:update`: send only the keys to change and the rest keep their values. </Accordion> </Accordions> ## Related - [The registry](/projects/registry) — the project's own fields: name, key, client, leads, dates - [Environments](/projects/environments) — the health checks the incident switch reads - [Notifications](/account/notifications) — where personal alerts go beside the project's Telegram group - [`project.*` webhook events](/webhooks/v1/events/project) --- # The Registry Source: https://alm-docs.labs.zuselab.dev/projects/registry By the end of this page you will have started a project from the registry, know every field it carries and what reads each one downstream, and know how a project is moved along its lifecycle, put away, brought back and, when it must be, deleted. The registry is the **Projects** page in the sidebar: every project of your team you may see, with the status and health of each, who manages it and when it is due. It is the first page of every phase, because every component, environment, epic, story, test, release and incident lives under one of its rows. ## Before you begin | You want to | You need | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | Open the registry | To belong to the team. Every default role sees it; a guest sees only the projects they were invited to. | | Create a project | `project:create` on your role, or to own the team. Admins and project managers have it by default. | | Edit, move or archive one | `project:update`, through your role on that project or your team role. | | Delete one | `project:delete`. | | Name a client on the project | The client to exist already; add it on the [Clients](/projects/clients) page first. | ## Read the registry [step] The table lists one project per row: | Column | What it shows | | ------------------- | ------------------------------------------------------------------------------------------------- | | **Key** | The project's key, the prefix of every reference inside it. | | **Project** | The name, with the tagline beneath it. Press the name to open the detail flyout. | | **Client** | The client the project is done for, or _Internal_. | | **Status** | The lifecycle status as a badge. | | **Health** | On track, at risk, off track or not assessed. | | **Project Manager** | The lead named on the project, or _Unassigned_. | | **Target Launch** | The date agreed with the client, when there is one. | | **Updated** | How long ago the record last changed. | | Row menu | **View Project**, **Edit Project**, **Archive Project** or **Restore Project**, **Activity**, **Delete Project**. | Sort by key, project, status, health, target launch or updated by pressing a column heading; press it again to flip the direction. Above the table, narrow the list with the search box (words in the key, the name or the tagline), the **status** list, the **client** list, and the **Archived** switch, which shows archived projects instead of live ones. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Start a project [step] 1. Press **New Project**. It opens the [intake wizard](/projects/intake-wizard) on its first step, with nothing filled in. 2. Type the **Name**, pick the **Client** (or leave _Internal project_ for work the team does for itself) and write the one-paragraph **Description** Heimdall drafts from. Leave the **Key** empty and one is proposed from the name, or type your own; it is upper-cased as you type and checked against every project the team ever had, archived and deleted ones included, because references such as `CRM-142` stay in people's hands after a project is gone. 3. Set how it runs, name the leads you know and the dates agreed, or leave them for later; the same fields as the editor are offered, and every one can change afterwards. 4. Press **Create Project**. The project appears in the registry at once, `project.created` goes out to every endpoint subscribed to it, and the wizard walks on to the project's components, environments, stakeholders and personas. <Callout type="info" title="The key freezes"> Until an artefact is numbered under the project, the key may still change from the editor. The moment the first story, requirement or test case takes a number, the key is locked: the editor shows it read-only and the API refuses a change, because every reference already carries it. </Callout> ## Open a project [step] Press a project's name, or **View Project** in its row menu, to open the detail flyout: the key and status at the top, then the client and health, how it runs, the four leads, the dates and budget, the description and the timestamps. From the flyout: - **Open Project** leaves the registry for the project itself: [the overview](/projects/overview), with the sidebar switched to the project's menu. The row menu offers the same entry. - **Move to** changes the lifecycle status in one step. Every status but _Archived_ is offered; archiving has its own button so the status it leaves is remembered. - **Edit** opens the editor on this project. - **Archive** puts the project away, or **Restore** brings an archived one back. - **Activity** opens the project's history: every change, who made it and where it came from. - **Delete** opens the confirmation. A link such as `/projects?show=<id>` opens the registry with that project's flyout already open; Heimdall's **View** buttons and the notifications use it. ## Edit a project [step] 1. Press **Edit Project** in the row menu, or **Edit** in the flyout. 2. Change what you need. The same rules as creating apply: the name stays unique, the key may only change while it is not yet locked, the client must be the team's and each lead a member. 3. Press **Save Changes**. A status changed inside the editor is announced as `project.status_changed`; every other change travels as `project.updated` with its old and new values. An edit that changes nothing announces nothing. ## Archive, restore and delete [step] - **Archive Project** sets the status to _Archived_, remembers the status it left, and takes the project out of the registry's default view. Everything under it is kept and stays readable. Switch on **Archived** above the table to find it. - **Restore Project** brings it back with the status it had before. - **Delete Project** removes the project with everything under it: components, environments, people, the planning record, work, tests, releases, incidents and tickets. The confirmation asks you to type the project's key, and the key becomes free to reuse afterwards. Archiving is the reversible alternative; prefer it. ## All the fields <Accordions> <Accordion title="Name"> Required, 3 to 100 characters, unique within the team regardless of case. Shown everywhere the project is named: the registry, the project selector, Heimdall's answers and every notification. </Accordion> <Accordion title="Key"> Required, two to six upper-case letters and digits starting with a letter, such as `CRM` or `POR2`. Unique across everything the team ever had. The wizard proposes one from the name when it is left empty. It prefixes every reference in the project, and it is locked once an artefact carries it; the editor then shows it read-only. </Accordion> <Accordion title="Client"> Optional. One of the team's clients, or _Internal project_. A client guest invited to the project sees this client on the Clients page. Which clients are offered comes from the Clients page. </Accordion> <Accordion title="Tagline"> Optional, up to 160 characters. One line under the name in the registry. </Accordion> <Accordion title="Description"> Optional rich text: headings, bold, lists and links. The one-paragraph brief Heimdall reads when it drafts the rest of the record. Filtered to a safe subset when shown. </Accordion> <Accordion title="Status"> Required. Discovery, planning, in development, in testing, live, maintenance or on hold. Archived is set by the archive action, never by hand. Each move is announced as `project.status_changed`. The [status reference](/reference/status-reference#project-status) says what each means. </Accordion> <Accordion title="Methodology"> Required. Scrum (sprints with a commitment and a review), Kanban (a continuous flow with WIP limits), Hybrid (sprints with a Kanban lane for support) or Waterfall (phases signed off one after another). The boards and sprints read it. </Accordion> <Accordion title="Visibility"> Required. **Whole team**: every member of the team sees the project, and a project role still decides what they may do. **Members only**: only the people added to the project see it. A guest always sees a project through a membership row. </Accordion> <Accordion title="Project Manager, Business Analyst, Lead Developer, QA Lead"> Optional, each a member of the team. The registry shows the project manager; the dashboards, the approvals and the digests read all four as they land. </Accordion> <Accordion title="Starts, Target Launch, Launched"> Optional dates. The target launch must be on or after the start. Launched is the day the project went live and is offered once the project exists. </Accordion> <Accordion title="Budget (hours)"> Optional whole hours, up to a million. Reports read logged time against it. </Accordion> <Accordion title="Health, created, updated and archived stamps"> Read-only in the flyout. Health is computed from the record; the stamps are kept by the application and shown in your own timezone. </Accordion> </Accordions> ## What happens next - Every create, edit, status move, archive, restore and delete is written to the project's [activity history](/workspace/activity-log) with who did it and where from, Heimdall included. - The `project.created`, `project.updated`, `project.status_changed`, `project.archived`, `project.restored` and `project.deleted` events go out to every endpoint subscribed to them. - A project's client appears on that client's record, and a client that has projects can no longer be deleted. ## Heimdall on this page Heimdall reads the registry through the same tools the MCP server offers, and its answers carry a **View** button that opens the project's flyout. Try: “Which projects are in development?” · “Who leads the CRM project?” · “Which projects have no target launch date yet?” Creating and shaping projects is a person's work: Heimdall and an agent over MCP read the registry and never write to it. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ------------------------------- | ------------------------------------------------------- | | See the registry | `project:view-any` | | Open a project | `project:view` | | Create a project | `project:create`, or owning the team | | Edit, move, archive or restore | `project:update`, through a project role or a team role | | Delete a project | `project:delete` | ## Frequently asked <Accordions> <Accordion title="Why can I not change the key any more?"> An artefact has been numbered under the project, so references such as `CRM-142` already carry the key. Changing it would orphan every one of them. Create a new project if the key must differ. </Accordion> <Accordion title="I see a project I cannot edit"> Your team role lets you see the project but not change it, or the project has a members-only visibility and your project role is a viewer's. Ask the project manager for a project role. </Accordion> <Accordion title="Where did an archived project go?"> Switch on **Archived** above the table. It is listed there with the status _Archived_, and **Restore Project** brings it back with the status it had. </Accordion> </Accordions> ## Related - [The project overview](/projects/overview) — the project's home once it is open - [Clients](/projects/clients) — the organisations projects are done for - [Status reference](/reference/status-reference) — every status and what moves a record into it - [Activity log](/workspace/activity-log) — where every change is recorded --- # Repositories Source: https://alm-docs.labs.zuselab.dev/projects/repositories By the end of this page you will have registered where a project's code lives, know every field a repository carries and what reads each one downstream, and know how a repository is set aside, brought back and, when it must be, deleted. A **repository** is a Git repository a project's code lives in: its provider, the account or group that owns it, its name, the branch releases are cut from, whether it is private and which [component](/projects/components) it builds. The registry is kept by hand for now; once Git hosting is connected the same rows carry the connection, and branches, commits and pull requests hang off them to link work items to code. Releases will read the default branch, deployments will name the repository, and Heimdall names it when it drafts a branch name for a story. The Repositories tab is the first of the project's **Code** pages; branches and pull requests join it when Git hosting lands. ## Before you begin | You want to | You need | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Open the Repositories tab | `repository:view-any` on the project. Every staff role has it; a client guest never does, and is refused at the door. | | Register a repository | `repository:create`, through your role on the project or your team role, or to own the team. Developers have it by default. | | Edit, disconnect or reconnect one | `repository:update`. | | Delete one | `repository:delete`. | The page lives under a project: open the project and choose **Code** in the sidebar. A client guest does not see the entry, and the page answers them with a refusal. ## Read the repositories [step] The table lists one repository per row, by full name: | Column | What it shows | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | **Repository** | The full name in the provider's spelling with the address beneath it, a _Private_ badge when the code is private and a _Disconnected_ badge when it is set aside. Press the name to open the detail flyout. | | **Provider** | Where it is hosted: GitHub, GitLab or Bitbucket, as a coloured badge. | | **Component** | The component it builds, or _Whole project_. | | **Default Branch** | The branch releases are cut from. | | **Updated** | How long ago the record last changed. | | Row menu | **View Repository**, **Open at Provider**, **Edit Repository**, **Disconnect Repository** or **Reconnect Repository**, **Activity**, **Delete Repository**. | Sort by repository, provider, default branch or updated by pressing a column heading; press it again to flip the direction. Above the table, narrow the list with the search box (words in the full name, the address or the default branch), the provider filter, the component filter and the **Disconnected** switch, which shows disconnected repositories instead of live ones. The filters travel in the page address; **Clear Filters** on the empty state drops them all. ## Register a repository [step] 1. Press **Register Repository**. 2. Pick the **Provider** and type the **Owner** (the account or group) and the **Name** as the provider spells them. The **Address** is derived from them; fill it in only for a self-managed host. 3. Under **How It Is Used**, set the **Default Branch** releases are cut from, pick the **Component** the repository builds or leave it as the whole project, and say whether it is a **Private repository**. 4. Press **Register Repository**. The repository appears in the table at once, you are recorded as the person who registered it, and `repository.created` goes out to every endpoint subscribed to it. <Callout type="info" title="The registry comes before the connection"> Registering a repository writes down where the code lives; it does not read the code. Connecting Git hosting, which links branches, commits and pull requests to work items, arrives with the Git integration and attaches to the rows you register here, so the registry is worth filling in now. </Callout> ## Open a repository [step] Press a repository's name, or **View Repository** in its row menu, to open the detail flyout: the provider, privacy and disconnected badges, the full name and address, **Open at Provider**, the **Default Branch**, the **Component**, who **Registered** it and when, and the updated or disconnected stamp. From the flyout, **Edit**, **Disconnect** or **Reconnect**, **Activity** and **Delete** are one press away. A link such as `/projects/<project>/code?show=<id>` opens the tab with that repository's flyout already open; notifications and Heimdall's buttons use it. ## Edit a repository [step] 1. Press **Edit Repository** in the row menu, or **Edit** in the flyout. 2. Change what you need. A new owner or name must leave the full name free within the project for that provider; clearing the address falls back to the provider's own. 3. Press **Save Changes**. Every change travels as `repository.updated` with its old and new values. An edit that changes nothing announces nothing. ## Disconnect, reconnect and delete [step] - **Disconnect Repository** sets the repository aside: it leaves the default view and the live listing while its row and history stay, and no branch, commit or pull request will be read from it. Switch on **Disconnected** above the table to find it. - **Reconnect Repository** brings a disconnected repository back into use. - **Delete Repository** removes the registration with its activity history after a confirmation; there is no undo. The code itself stays with the provider. Branches, commits and pull requests are not here yet; once they are, a repository with code references will be disconnected rather than deleted. ## All the fields <Accordions> <Accordion title="Provider"> Required, _GitHub_, _GitLab_ or _Bitbucket_; GitHub by default. The [status reference](/reference/status-reference#git-provider) says what each is for. The provider decides the derived address and which integration will connect the repository. </Accordion> <Accordion title="Owner and Name"> Required, up to 100 characters each of letters, digits, dots, dashes and underscores, as the provider spells them. Together they make the full name, which is unique per provider within the project regardless of case. </Accordion> <Accordion title="Address"> Optional, an `http` or `https` link of up to 2,048 characters. Left empty, the provider's own address for the owner and name is used; fill it in for a self-managed host. </Accordion> <Accordion title="Default Branch"> Required, up to 255 characters without spaces; `main` by default. The branch releases are cut from and feature branches start at; the Git integration and the release pages read it. </Accordion> <Accordion title="Component"> Optional, one of the project's live components. The application this repository builds; leave it empty when it serves the whole project. The component filter reads it. </Accordion> <Accordion title="Private repository"> Whether the code is private at the provider, on by default. A public repository can be linked without a connection; a private one needs the Git integration's access. </Accordion> <Accordion title="Full name, component, registered by and stamps"> Read-only facts the flyout shows: the full name as stored, the component's name, who registered the repository, and the registered, disconnected, created and updated stamps in your own timezone. </Accordion> </Accordions> ## What happens next - Every register, edit, disconnect, reconnect and delete is written to the repository's [activity history](/workspace/activity-log) with who did it and where from. - The `repository.created`, `repository.updated`, `repository.disconnected`, `repository.reconnected` and `repository.deleted` events go out to every endpoint subscribed to them. - The Git integration will attach to these rows, and releases and deployments will name them, as those modules land. ## Heimdall on this page Heimdall reads the repositories through the same tools the MCP server offers and carries the open project as its context. Try: “Where does the web app's code live?” · “Which repositories cut releases from a branch other than main?” · “Suggest a branch name for the checkout story in the right repository.” Registering repositories is a person's work: an agent over MCP reads the registry to clone, branch and name repositories correctly, and never rewrites it. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | -------------------------------- | --------------------------------------------------------------- | | See the repositories | `repository:view-any`; a client guest is refused | | Open a repository | `repository:view` | | Register a repository | `repository:create`, or owning the team | | Edit, disconnect or reconnect | `repository:update`, through a project role or a team role | | Delete a repository | `repository:delete` | ## Frequently asked <Accordions> <Accordion title="Why can a client guest not see the repositories?"> Where the code lives is the team's business: a repository's name and host say nothing a client needs for sign-off and may say something about the team's other work. Guests see the environments, the designs and the approved planning record instead. </Accordion> <Accordion title="Should I register a repository before Git hosting is connected?"> Yes. The registry is what the connection attaches to, and releases, deployments and Heimdall read it in the meantime; the connection adds branches and pull requests on top. </Accordion> <Accordion title="Should I delete a repository the team stopped using?"> Disconnect it. It keeps its history and comes back with one press if the code is picked up again; delete only a registration that was wrong. </Accordion> </Accordions> ## Related - [Components](/projects/components) — what a repository builds - [The project overview](/projects/overview) — the project the repositories belong to - [Status reference](/reference/status-reference#git-provider) — the three providers - [`repository.*` webhook events](/webhooks/v1/events/repository) --- # Stakeholders Source: https://alm-docs.labs.zuselab.dev/projects/stakeholders By the end of this page you will have written down who a project answers to, know every field a stakeholder carries and what reads each one downstream, know how the four approval flags steer the approvals, sign-offs and reports to come, and know what a client guest is shown of the record. A **stakeholder** is a person the project answers to: the client's sponsor who pays for it, the product owner who ranks the backlog, the people who sign off requirements, UAT runs and releases, Zuse's own account manager or architect, and third parties such as an auditor or a vendor. A stakeholder is a row on the record, not an account; the sponsor is on it whether or not they ever sign in, and when they do sign in as a guest the row is linked to their account so the approvals that name them reach the person who decides. The Stakeholders tab is the first of the project's **People** pages; the second is [Personas](/projects/personas), who the project is built for. ## Before you begin | You want to | You need | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Open the Stakeholders tab | `stakeholder:view-any` on the project. Every staff role has it; a client guest has it through their project membership. | | Add a stakeholder | `stakeholder:create`, through your role on the project or your team role, or to own the team. Business analysts and project managers have it by default. | | Edit one | `stakeholder:update`. | | Remove one | `stakeholder:delete`. | | Link a stakeholder to an account | The person to be a member of the team already, usually a guest [invited to the project](/workspace/members-and-guests). | The page lives under a project: open the project from the selector or the registry and choose **People** in the sidebar's **Selected Project** group. The entry is badged with the number of stakeholders on the record, and the tab strip above the table switches between **Stakeholders** and **Personas**. ## Read the stakeholders [step] The table lists one stakeholder per row, in the project's order: | Column | What it shows | | --------------- | ------------------------------------------------------------------------------------------------------------------------------ | | **Stakeholder** | The name, with the title and the organisation beneath it. Press the name to open the detail flyout. | | **Side** | Whose side they speak for: client, Zuse or a third party, as a coloured badge. | | **Role** | What they do for the project: sponsor, product owner, architect and so on, with its icon. | | **Weight** | How much their word carries and how closely they follow the project, as two badges, and their default RACI letter. Staff only. | | **Approves** | Which approvals name them: _Requirements_, _UAT_ and _Releases_, or _Nothing_. | | **Updated** | How long ago the record last changed. | | Row menu | **View Stakeholder**, **Edit Stakeholder**, **Activity**, **Remove Stakeholder**. | Sort by stakeholder, side, role, weight or updated by pressing a column heading; press it again to flip the direction. Above the table, narrow the list with the search box (words in the name, the email, the organisation or the title), the **side** list, the **role** list and the **approval** list, which keeps only the people that approval names. The filters travel in the page address, so a filtered list can be shared; **Clear Filters** on the empty state drops them all. ## Add a stakeholder [step] 1. Press **New Stakeholder**. 2. Type the **Name**, and the **Title** and **Organisation** if you know them. 3. Pick the **Side**: _Client_ for the people who commissioned the project, _Zuse_ for the people on your own side who speak for it, _Third party_ for everyone else. 4. Add the **Email** and the **Phone** under **Contact**. The email is unique among the project's stakeholders and is where approvals and reports will reach them. 5. Under **Role and Weight**, pick the **Role**, rate their **Influence** and **Interest**, and set a **RACI Default** if they will have a standing letter on the project's epics. 6. Under **Approvals**, switch on what they decide: **Approves requirements**, **Signs off UAT**, **Approves releases**, and whether they **Receive status reports**. 7. If they already have an account, pick it under **Linked Account**. 8. Put what the team should know in the **Notes**: how they like to be reached, what they care about, what to avoid. A guest never sees them. 9. Press **Add Stakeholder**. The stakeholder appears in the table at once and `stakeholder.created` goes out to every endpoint subscribed to it. <Callout type="info" title="A name is all it needs"> The side defaults to the client, the role to _Other_, both ratings to _Medium_ and the three approval flags to off; status reports are on. Fill in the rest as the intake proceeds, and let Heimdall propose the sponsor, the product owner and the approvers from the brief. </Callout> ## Open a stakeholder [step] Press a stakeholder's name, or **View Stakeholder** in its row menu, to open the detail flyout: the side and role badges, the name with the title and organisation, the four **Approves** lines with a tick on each that applies, then for staff the **Email** and **Phone**, the **Influence**, **Interest** and **RACI Default** badges, the **Linked Account** and the **Notes**, and the created and updated stamps. From the flyout, **Edit**, **Activity** and **Remove** are one press away. A link such as `/projects/<project>/people?show=<id>` opens the tab with that stakeholder's flyout already open; notifications and Heimdall's buttons use it. ## Edit a stakeholder [step] 1. Press **Edit Stakeholder** in the row menu, or **Edit** in the flyout. 2. Change what you need. The same rules as adding apply: the email stays unique within the project, and a linked account must be a member of the team and linked to one stakeholder only. 3. Press **Save Changes**. Every change travels as `stakeholder.updated` with its old and new values. An edit that changes nothing announces nothing. ## Remove a stakeholder [step] **Remove Stakeholder** in the row menu, or **Remove** in the flyout, opens a confirmation. Removing takes the stakeholder and their activity history off the record; there is no undo. Approvals that will name stakeholders are not here yet; once they are, a stakeholder with decisions on the record will be kept rather than removed. ## What a guest sees A client guest invited to the project sees the Stakeholders tab with every stakeholder on it, because the client knows who speaks for them: the name, title and organisation, the side, the role and the four approval lines. The email and phone, the influence and interest, the RACI default, the linked account and the notes are not shown, and the guest's own API token leaves those fields out of the payload rather than nulling them. ## All the fields <Accordions> <Accordion title="Name"> Required, 2 to 120 characters. As the person is known to the client and the team. Shown wherever the stakeholder is named: this tab, the approvals that will ask them, the RACI on each epic, the status reports. </Accordion> <Accordion title="Title and Organisation"> Optional, up to 120 characters each. The job title and the organisation they belong to, in words, shown beneath the name. </Accordion> <Accordion title="Side"> Required. _Client_, _Zuse_ or _Third party_. Says whose sign-off a stakeholder's approval is and groups the people page. The [status reference](/reference/status-reference#stakeholder-side) says what each side is for. </Accordion> <Accordion title="Email and Phone"> Optional. The email is a valid address, unique among the project's stakeholders regardless of case; the phone is up to 40 characters. Staff only. The email is where approvals and reports will reach a stakeholder who has no account. </Accordion> <Accordion title="Role"> Required. Sponsor, product owner, business owner, subject matter expert, end-user representative, technical lead, architect, security, legal, finance, operations or other. The analyst's shorthand for why the person is on the record; it carries no permission. </Accordion> <Accordion title="Influence and Interest"> Required, each _Low_, _Medium_ or _High_. How much weight the person's word carries and how closely they follow the project: the two axes of a power/interest grid, read by the reports and the digests. Staff only. </Accordion> <Accordion title="RACI Default"> Optional. _Responsible_, _Accountable_, _Consulted_ or _Informed_: the letter the person gets on an epic unless the epic says otherwise. Staff only. </Accordion> <Accordion title="Approves requirements, Signs off UAT, Approves releases"> Three flags, off by default. They say which approvals will name the stakeholder: requirement reviews and client sign-offs, UAT sign-offs, and go-live approvals. **Approves requirements** already does two things once a guest account is linked to the seat: the seat can be named on a requirement review by the project's policy, and it is asked, with every other seat so flagged, to sign a scope [baseline](/planning/baselines) after the team froze it. A [change request](/planning/change-requests) raised for the stakeholder names their seat among its approvers whatever the flags say. The other two flags are read as their modules land. </Accordion> <Accordion title="Receives status reports"> A flag, on by default. Status reports and the weekly digest will go to the stakeholder. </Accordion> <Accordion title="Linked Account"> Optional, a member of the team, usually a guest invited to the project. One account links to one stakeholder per project. Approvals that name the stakeholder reach this account. Staff only. </Accordion> <Accordion title="Notes"> Optional rich text, up to 20,000 characters, staff only: how they like to be reached, what they care about, what to avoid. </Accordion> <Accordion title="Created and updated"> Read-only stamps kept by the application and shown in your own timezone. </Accordion> </Accordions> ## What happens next - Every add, edit and removal is written to the stakeholder's [activity history](/workspace/activity-log) with who did it and where from. - The `stakeholder.created`, `stakeholder.updated` and `stakeholder.deleted` events go out to every endpoint subscribed to them. - The approval policies, the RACI on each epic, the status reports and the digests will read the flags, the ratings and the RACI default as those modules land, which is why they are captured now. ## Heimdall on this page Heimdall reads the stakeholders through the same tools the MCP server offers and carries the open project as its context. Try: “Who signs off UAT on this project?” · “List the client-side stakeholders and their roles.” · “Who should I ask about the invoicing rules?” Adding and shaping stakeholders is a person's work: Heimdall and an agent over MCP read the record and never write to it. ## From the API and MCP <Cards> </Cards> ## Permissions | Action | Permission | | ---------------------------- | --------------------------------------------------------------------------------- | | See the stakeholders | `stakeholder:view-any`; a guest sees them without the internal fields | | Open a stakeholder | `stakeholder:view` | | Add a stakeholder | `stakeholder:create`, or owning the team | | Edit a stakeholder | `stakeholder:update`, through a project role or a team role | | Remove a stakeholder | `stakeholder:delete` | | Read or restore its versions | `stakeholder:view` to read, `stakeholder:update` to restore; never a client guest | ## Frequently asked <Accordions> <Accordion title="The email is refused"> Another stakeholder of the project already carries it, regardless of case. Two rows for one person are a mistake; open the existing one instead. </Accordion> <Accordion title="The account I want to link is not offered"> Only members of the team are offered. Invite the person as a guest of the project first, and link the account once they have accepted. </Accordion> <Accordion title="Is a stakeholder the same as a project member?"> No. A stakeholder is a person on the record, with or without an account; a project member is an account with a role on the project. A client guest is usually both, linked through the account field. </Accordion> </Accordions> ## Related - [Personas](/projects/personas) — who the project is built for - [Members and guests](/workspace/members-and-guests) — inviting a stakeholder as a guest - [Status reference](/reference/status-reference#stakeholder-side) — sides, ratings and RACI letters - [Revisions and versions](/planning/revisions-and-versions) — every save kept, any version put back - [`stakeholder.*` webhook events](/webhooks/v1/events/stakeholder) --- # Changelog Source: https://alm-docs.labs.zuselab.dev/reference/changelog This page is the record of what changed in Zuse Bifrost and when, newest first. Each entry names the day a change reached the product, says in a sentence what a person or an agent can do that they could not do before, and links to the pages of this guide that describe it in full. Read it when the product has moved under you, or when a page you know has grown a section you have not seen. <Callout type="info" title="Dated entries now, numbered releases later"> Zuse Bifrost is still being built out phase by phase, so every entry below is dated rather than numbered. Once the product is live, each entry carries the version it shipped in, and the dates stay beside the versions. </Callout> ## October 29, 2026 - **The board.** The running sprint's work, or any scope of the backlog, in a column per status: a card dragged into another column runs the same move as the backlog, a card dragged up or down takes a new place in the project's order, a column the project caps turns red and takes no more once full, the swimlanes lay the cards out by epic, person or kind, and the quick filters narrow them. Agents read the board through their own tool, items are ranked over the API, and `work_item.ranked` joins the family. See [Board](/building/board), [Backlog](/planning/backlog) and [Project settings](/projects/project-settings). - **Sprints.** A sprint is planned with its dates, goal and capacity, takes its scope from the backlog, starts when the team's commitment approval carries, runs with its points on the page, and closes with its velocity recorded and the unfinished work carried to the backlog or the next sprint. The backlog gained a sprint field and a sprint filter, agents read sprints through their own tools, and the `sprint.*` events go out to webhook endpoints. See [Sprints](/building/sprints), [Backlog](/planning/backlog) and [Approvals](/planning/approvals). - **Generated documents.** A business requirements document or a software requirements specification is rendered from the record, drafted by Heimdall, approved to final and shared with the client, with every copy kept. See [Generated documents](/documents/generated-documents), [BRD](/documents/brd), [SRS](/documents/srs) and [Documents shared with you](/guests/documents-shared-with-you). - **Baselines and change requests.** A baseline freezes a set of approved records, the client signs off on scope, and a change request is the one way a frozen record changes, with Heimdall's impact analysis and the council's change-impact sitting. See [Baselines](/planning/baselines), [Change requests](/planning/change-requests), [Signing off scope](/guests/signing-off-scope) and [Impact analysis](/copilot/impact-analysis). - **Heimdall's panel.** The panel says what Heimdall is doing while it works, grew wider, lights its aura from the edges of the window, follows the record it writes to its page, and folds the answer's cards. The council now deliberates: every voice researches its lens, reads the record, answers the others and refines the plan before Heimdall rules. See [The Heimdall panel](/copilot/panel), [The council](/copilot/council) and [Start a project with Heimdall](/copilot/start-a-project). ## October 28, 2026 - **Approvals.** The approval engine landed with its policies, requests, signed decisions and clocks, the approval bar on every approvable record, the policy editor in the project settings and the approvals inbox, over the REST API and the MCP server as well. See [Approvals](/planning/approvals), [Approving requirements](/guests/approving-requirements) and [Project settings](/projects/project-settings). - **A project from a conversation.** Heimdall starts a project from a paragraph, writing the registry and the planning record as it goes, with record cards in the thread and the council sitting in it; agents gained the tools that write the registry, the record, the review queue and the research notes. See [Start a project with Heimdall](/copilot/start-a-project) and the [tools reference](/mcp/v1/tools-reference). - **Heimdall suggests.** The project overview gained the suggestions rail, the weekly improvement sweep checks the record for what is missing, and research runs read the project's domain. See [Project overview](/projects/overview), [The improvement sweep](/copilot/improvement-sweep) and [Research](/copilot/research). - **The council.** Named voices with their own lenses sit on bounded moments, each sitting kept with its turns and findings and a verdict after one rebuttal round, with a Council tab in the panel and the voices seated per project in its settings. See [The council](/copilot/council). - **Brief import.** A pasted brief, an upload or a knowledge entry is read into a review queue where every suggestion lights up the passage it came from, with an editor and a merge picker; nothing is written until a person accepts it. See [Brief import](/planning/brief-import) and [Heimdall reads a brief](/copilot/brief-import). - **Quality reviews.** Heimdall scores a story, a requirement or a rule, shows its findings in the flyout with Review now and Ask Heimdall, and badges the rows with the score. See [Quality review](/planning/quality-review) and [Quality review with Heimdall](/copilot/quality-review). - **Inline assists.** Ask Heimdall sits beside every long field of the planning editors for one metered rewrite, and a version saved after an inline draft is marked as drafted by Heimdall and accepted by the person. See [Inline assist](/copilot/inline-assist) and [Revisions and versions](/planning/revisions-and-versions). - **Versions for the registry.** Glossary terms, data entities, personas and stakeholders keep every version and show them in their flyouts. See [Glossary](/projects/glossary), [Data dictionary](/projects/data-dictionary), [Personas](/projects/personas) and [Stakeholders](/projects/stakeholders). - **Around every artefact.** Attachments on a private disk with a files panel, labels as a project's own vocabulary on the backlog, comments with mentions and watchers under every record, and links between artefacts read from either side. See [Attachments](/planning/attachments), [Labels](/planning/labels), [Comments and mentions](/planning/comments-and-mentions) and [Traceability](/planning/traceability). - **The rest of the planning record.** Decisions with the options weighed, the RAID log with its risk matrix, use cases with their flows and steps, and business rules of six kinds with condition, action, exceptions and worked examples. See [Decisions](/planning/decisions), [RAID log](/planning/raid-log), [Use cases](/planning/use-cases) and [Business rules](/planning/business-rules). - **The brand.** The application and this guide draw their marks and icons from the Zuse Bifrost logo. ## October 27, 2026 - **Requirements.** Functional and non-functional requirements on their own sequences, versioned, on their page and over the REST API and the MCP server. See [Functional requirements](/planning/functional-requirements) and [Non-functional requirements](/planning/non-functional-requirements). - **The backlog.** Work items of six kinds with acceptance criteria, versioned, with their lifecycle, hand-over and versions. See [Backlog](/planning/backlog) and [Acceptance criteria](/planning/acceptance-criteria). - **Versions and epics.** Every save of a revisable artefact writes a restorable version with its diff, author, origin and reason; epics are numbered from the tracker sequence with their lifecycle and history. See [Epics](/planning/epics) and [Revisions and versions](/planning/revisions-and-versions). - **The intake wizard.** A project starts from a client, a name and a paragraph, walks components, environments, stakeholders and personas with presets, and resumes from the sidebar until it is finished. See [Intake wizard](/projects/intake-wizard). - **The registry.** Project settings with workspace defaults and a danger zone, repositories, design references, the dictionary of glossary terms and data entities, stakeholders and personas, and components and environments with health checks and change freezes. See [Project settings](/projects/project-settings), [Repositories](/projects/repositories), [Design references](/projects/design-references), [Glossary](/projects/glossary), [Data dictionary](/projects/data-dictionary), [Stakeholders](/projects/stakeholders), [Personas](/projects/personas), [Components](/projects/components) and [Environments](/projects/environments). - **The project shell.** The project selector, the sidebar menus, the overview page, the project registry and the clients page. See [Project overview](/projects/overview), [Registry](/projects/registry) and [Clients](/projects/clients). - **Names and voice.** The product is called Zuse Bifrost and its assistant Heimdall everywhere people read them, and the application's prose took on a light Norse voice with the nouns kept plain. See the [introduction](/introduction). - **Heimdall with a budget.** The assistant runs behind AI metering with monthly budgets per team and the AI settings page. See [The Heimdall panel](/copilot/panel), [Usage and budgets](/copilot/usage-and-budgets) and [AI settings](/workspace/ai-settings). ## October 26, 2026 - **Machine surfaces.** The REST API with its OpenAPI document, the MCP server with OAuth, discovery documents, a consent screen, the first tools and the catalogue resources, and the agent-readiness documents served from the application. See the [REST API](/api/v1/index), the [MCP server](/mcp/v1/index) and [Agent readiness](/api/v1/agent-readiness). - **Events and webhooks.** Domain events announce every change and outbound webhooks deliver them, signed, with retries. See [Webhooks and events](/webhooks/v1/index) and [Webhook endpoints](/workspace/webhooks). - **Activity and notifications.** The activity log and timeline, the notifications centre, alert destinations per class, and Telegram linking. See [Activity log](/workspace/activity-log), [Notifications](/account/notifications) and [Telegram](/integrations/telegram). - **The workspace.** Teams, members, invitations, roles and groups, with API tokens and webhook settings. See [Teams](/workspace/teams), [Members and guests](/workspace/members-and-guests), [Roles](/workspace/roles), [Groups](/workspace/groups) and [API tokens](/workspace/api-tokens). - **Accounts.** Invite-only sign-in with two-factor authentication and passkeys, and the account settings pages. See [Account](/account/index), [Sign in](/account/sign-in), [Two-factor authentication](/account/two-factor-auth) and [Passkeys](/account/passkeys). --- # Status Reference Source: https://alm-docs.labs.zuselab.dev/reference/status-reference Every status in Zuse Bifrost is a fixed set of named states, shown as a coloured badge wherever the record appears and sent as a lower-case code over the REST API, the MCP server and the webhook events. This page lists each set, what every state means and what moves a record into it. Statuses are never user-defined: a project configures which board columns it shows and what a checklist demands, not new states. <Callout type="info" title="Codes and labels"> The badge shows the label in the first column; the API, the MCP tools and the event payloads carry the code in the second. A filter on a list page takes the code. </Callout> ## Client status The lifecycle of a client account, set by hand on the client's record. | Label | Code | Meaning | | ------------ | ---------- | ------------------------------------------------------------------------------------------ | | **Prospect** | `prospect` | A client you are talking to, with no project started yet. | | **Active** | `active` | A client with at least one project in flight; the default for a client with live work. | | **Dormant** | `dormant` | A client with no active project at the moment, kept for its history and a possible return. | | **Churned** | `churned` | A client that has left; their projects stay readable, nothing new is started under them. | ## Project status Where a project stands in its lifecycle, set on the project's record and shown in the registry, the project selector and Heimdall's answers. | Label | Code | Meaning | | ------------------ | ---------------- | -------------------------------------------------------------------------------------------- | | **Discovery** | `discovery` | The brief is being gathered; the intake has started and the scope is not settled. | | **Planning** | `planning` | The scope is being written down and approved: epics, stories, requirements, rules. | | **In development** | `in_development` | Sprints are running against the approved backlog. | | **In testing** | `in_testing` | The build is under test: plans are executed, defects raised, UAT prepared. | | **Live** | `live` | The product is in production use. | | **Maintenance** | `maintenance` | Live and under a maintenance arrangement: incidents, tickets and change requests come first. | | **On hold** | `on_hold` | Paused by the client or by Zuse; nothing moves until it is resumed. | | **Archived** | `archived` | Closed. Read-only, left out of lists unless archived projects are asked for. | ## Project health A summary the application recomputes from the record, never set by hand: overdue work, failing test runs, open severe incidents and stalled approvals each weigh on it as the modules that raise them land. | Label | Code | Meaning | | ---------------- | ----------- | -------------------------------------------------------------------------------------- | | **On track** | `on_track` | Nothing overdue, nothing failing, nothing stuck. | | **At risk** | `at_risk` | Something needs attention this week: an overdue item, a failing run, a stuck approval. | | **Off track** | `off_track` | Several signals at once, or a severe incident open past its SLA. | | **Not assessed** | `unknown` | Too new or too empty to judge; the badge stays grey until the record fills in. | ## Environment kind What an [environment](/projects/environments) is for, set on its record. The kinds are also the order the table lists environments in and the order releases will promote through; a client guest is shown the UAT and production environments only. | Label | Code | Meaning | | ------------------ | --------------- | ---------------------------------------------------------------------- | | **Local** | `local` | A developer's own machine; listed so its conventions are written down. | | **Development** | `development` | The shared environment the team deploys work in progress to. | | **QA** | `qa` | Where the QA team executes test runs against a build. | | **Staging** | `staging` | A production-like environment for final checks before a release. | | **UAT** | `uat` | Where the client tests and signs off what was built. | | **Pre-production** | `preproduction` | A mirror of production for rehearsing releases and migrations. | | **Production** | `production` | The live environment the client's users depend on. | | **Custom** | `custom` | An environment none of the other kinds describe. | ## Environment health What the last check of an environment's health check address found, or what a monitoring system reported over the API. Written by the application, never set by hand; a change from one health to another is announced as `environment.health_changed`. | Label | Code | Meaning | | --------------- | ---------- | ------------------------------------------------------------------------------ | | **Healthy** | `healthy` | The address answered with a success status. | | **Degraded** | `degraded` | The address answered, but not with a success status: a redirect, a 404, a 401. | | **Down** | `down` | The address answered with a server error or did not answer within the timeout. | | **Not checked** | `unknown` | No health check address is set, or the first check has not run yet. | ## Stakeholder side Whose side a [stakeholder](/projects/stakeholders) speaks for, set on their record. The side says whose sign-off their approval is and groups the people page. | Label | Code | Meaning | | --------------- | ------------- | ------------------------------------------------------------------------------------------------ | | **Client** | `client` | Someone on the client's side: the sponsor, the product owner, the people who sign off. | | **Zuse** | `zuse` | Someone at Zuse who speaks for the project: an account manager, an architect, a department head. | | **Third party** | `third_party` | A vendor, an auditor, a regulator, a partner: anyone outside both organisations. | ## Influence, interest and proficiency One three-step scale rates a stakeholder's influence and interest and a persona's tech proficiency, so the badges read the same wherever a rating appears. | Label | Code | Meaning | | ---------- | -------- | ---------------------------------------------------------------------------------------- | | **Low** | `low` | Little weight, little attention, or little ease with new software. | | **Medium** | `medium` | The default: worth keeping informed, follows the project, manages everyday tools. | | **High** | `high` | Their word decides, they follow every change, or they take to new software without help. | ## RACI letter A stakeholder's default part in a RACI matrix: the letter they get on an epic unless the epic says otherwise. Set on the stakeholder's record, staff only. | Label | Code | Letter | Meaning | | --------------- | ------------- | ------ | ------------------------------------------------- | | **Responsible** | `responsible` | R | Does the work or sees that it is done. | | **Accountable** | `accountable` | A | Answers for the outcome; one person per decision. | | **Consulted** | `consulted` | C | Is asked before a decision is made. | | **Informed** | `informed` | I | Is told once a decision is made. | ## Artefact status The review of a reference artefact: a [glossary term](/projects/glossary), a [data entity](/projects/data-dictionary) or a [use case](/planning/use-cases). A term or an entity is moved by hand from the list or the flyout, or through the status endpoints; a use case reaches in review and approved through its [approval](/planning/approvals) and is moved by hand otherwise. A client guest sees an artefact while it is approved and not otherwise. | Label | Code | Meaning | | -------------- | ------------ | ------------------------------------------------------------ | | **Draft** | `draft` | Being written; the team reads it, the client does not yet. | | **In review** | `in_review` | Put up for review; waiting for the people who approve it. | | **Approved** | `approved` | Agreed and read by everyone, the client included. | | **Deprecated** | `deprecated` | Retired; kept for the record and no longer used in new work. | Draft, in review and approved may each move to any other status, the review states of a use case only through its approval; a deprecated artefact can only go back to draft, so a retired word, entity or use case is rewritten before it is approved again. ## Design reference kind What a [design reference](/projects/design-references) points at, set when the link is added. The badge colour groups the cards on the Designs page and the kind filter reads it. | Label | Code | Meaning | | -------------------- | ------------------ | -------------------------------------------------------------- | | **Figma** | `figma` | A Figma file or frame: the designs as the designer keeps them. | | **Wireframe** | `wireframe` | A low-fidelity sketch of a screen or a flow. | | **Prototype** | `prototype` | A clickable prototype the client can walk through. | | **Screenshot** | `screenshot` | A picture of a screen as it is or as it should be. | | **API contract** | `api_contract` | An OpenAPI or similar contract the developers build against. | | **Sequence diagram** | `sequence_diagram` | A diagram of who calls whom, in what order. | | **Entity diagram** | `erd` | A diagram of the entities and how they relate. | | **Document** | `document` | A written document: a spec, a brief, a guideline. | | **Other** | `other` | Anything else worth a link; the default when nothing fits. | ## Git provider Where a [repository](/projects/repositories) is hosted, set when it is registered. The provider decides the address derived from the owner and the name, and which integration will connect the repository once Git hosting lands. | Label | Code | Meaning | | ------------- | ----------- | ----------------------------------------------------------------------- | | **GitHub** | `github` | Hosted on GitHub; the GitHub App connects it first. The default. | | **GitLab** | `gitlab` | Hosted on GitLab, cloud or self-managed; the connection follows GitHub. | | **Bitbucket** | `bitbucket` | Hosted on Bitbucket Cloud; the connection follows GitHub. | ## Webhook delivery status What became of one event posted to one endpoint, shown on the [deliveries log](/workspace/webhooks) and returned by the deliveries endpoint and tools. | Label | Code | Meaning | | ------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------- | | **Pending** | `pending` | Queued or between attempts; the next attempt is scheduled. | | **Delivered** | `delivered` | The endpoint answered with a success status; the delivery is finished. | | **Failed** | `failed` | The last attempt was refused or timed out and another attempt is still due. | | **Dead** | `dead` | Every attempt failed, or the endpoint's allowed addresses refused the host; it is dead-lettered until someone retries it. | ## Heimdall action status What became of a tool Heimdall ran or asked leave to run, shown on the work timeline and the confirmation cards in the [panel](/copilot/panel). | Label | Code | Meaning | | ------------------------- | ----------- | --------------------------------------------------------------------------------------- | | **Ran** | `ran` | A read or an additive tool that ran at once and answered. | | **Awaiting confirmation** | `pending` | A destructive tool held behind a card, waiting for **Confirm** or **Cancel**. | | **Confirmed** | `confirmed` | You confirmed the card; the tool ran and Heimdall finished its answer. | | **Cancelled** | `rejected` | You cancelled the card; nothing ran. | | **Expired** | `expired` | Nobody answered the card within ten minutes; ask again and Heimdall prepares it afresh. | | **Failed** | `failed` | The tool threw before it could answer; nothing was changed by it. | ## Epic status Where an [epic](/planning/epics) stands, from the first draft to done. In review and approved are reached through the epic's [approval](/planning/approvals); the other moves are made by hand from the list or the flyout, or through the status endpoint. A client guest sees an epic once it is approved and from then on, through in progress and done, and loses it when it is cancelled. | Label | Code | Meaning | | --------------- | ------------- | ---------------------------------------------------------- | | **Draft** | `draft` | Being written; the team reads it, the client does not yet. | | **In review** | `in_review` | Put up for review; waiting for the people who approve it. | | **Approved** | `approved` | Agreed as scope and read by everyone, the client included. | | **In progress** | `in_progress` | Being built; its stories are on the board. | | **Done** | `done` | Delivered; every story under it is done. | | **Cancelled** | `cancelled` | Dropped; kept for the record and never built. | A draft is submitted for review or cancelled; a review is approved or returned to where it stood by the approvers; an approved epic is submitted again when it changes, goes on to in progress or to cancelled; one in progress goes to done or cancelled; a done epic may be reopened to in progress; a cancelled one is revived as a draft. ## MoSCoW priority The priority of a planning artefact, agreed with the client in four words. Epics, requirements, business rules and use cases carry one. The badge shows the letter; the sprint planner and the release composer read the rank. | Label | Code | Letter | Meaning | | --------------- | -------- | ------ | ------------------------------------------------------------------- | | **Must have** | `must` | M | The release cannot ship without it. | | **Should have** | `should` | S | Important, and the release ships without it only with a reason. | | **Could have** | `could` | C | Welcome if time allows; the first thing to drop. | | **Won't have** | `wont` | W | Agreed to be left out this time, written down so nobody asks again. | ## Work item kind What a row of the [backlog](/planning/backlog) is. All six kinds share the project's tracker sequence and the columns every item carries; a kind adds its own fieldset in the editor and the record keeps only the fields of the kind it is. The badge is painted in the kind's colour and carries its icon. | Label | Code | Meaning | | --------------- | ------------- | ---------------------------------------------------------------------------------- | | **Story** | `story` | Something a persona wants, with the criteria that say when it is done. | | **Task** | `task` | A unit of work that is not a story in itself. | | **Sub-task** | `subtask` | A piece of a story, a task, a bug or an improvement; it must have a parent. | | **Bug** | `bug` | Something that does not work as agreed, with its severity and how to reproduce it. | | **Spike** | `spike` | Time-boxed research or a prototype that ends in an outcome, not a feature. | | **Improvement** | `improvement` | A small change to something that already works. | A sub-task must hang under a parent and a bug may; a story, a task, a bug or an improvement may be that parent. ## Work item status Where a work item stands, from the backlog to done. Moved from the Backlog page, the flyout, the status endpoint or the `move_work_item` tool; the board, the Git integration and the Definition of Done will move items too as they land. A client guest sees an item once an approval pins it. | Label | Code | Meaning | | --------------- | ------------- | --------------------------------------------- | | **Backlog** | `backlog` | Written down and waiting; not yet planned. | | **Ready** | `ready` | Refined and ready to be picked up. | | **In progress** | `in_progress` | Being built. | | **In review** | `in_review` | Built and in code review. | | **In QA** | `in_qa` | With QA, being verified against its criteria. | | **Done** | `done` | Finished and accepted. | | **Cancelled** | `cancelled` | Dropped; kept for the record with the reason. | From the backlog an item goes to ready, in progress or cancelled; from ready back to the backlog, on to in progress or to cancelled; from in progress to ready, in review, in QA, done or cancelled; from in review to in progress, in QA, done or cancelled; from in QA to in progress, done or cancelled; a done item may be reopened to in progress; a cancelled one goes back to the backlog. The first move into progress stamps when work started and a move into done stamps the finish; a move into done or cancelled carries a resolution and a move into an open status clears it. ## Resolution Why a work item closed; set by the move into done or cancelled and cleared by a move back into an open status. Done takes `done`; cancelled takes any of the other four, won't do unless the move says otherwise. | Label | Code | Meaning | | -------------------- | ------------------ | -------------------------------- | | **Done** | `done` | Finished and accepted. | | **Won't do** | `wont_do` | Agreed not to do it. | | **Duplicate** | `duplicate` | Already covered by another item. | | **Cannot reproduce** | `cannot_reproduce` | The bug could not be reproduced. | | **Deferred** | `deferred` | Put off to a later release. | ## Work priority How urgent a work item is, in five steps. The backlog sorts by it and the sprint planner reads it; the badge carries an arrow pointing the way. | Label | Code | Meaning | | ----------- | --------- | -------------------------- | | **Lowest** | `lowest` | Whenever there is time. | | **Low** | `low` | After the medium items. | | **Medium** | `medium` | The ordinary case. | | **High** | `high` | Ahead of the medium items. | | **Highest** | `highest` | Before anything else. | ## Bug severity How badly a bug hurts, kept on bugs only. A blocker or a critical bug in a release's scope will hold the release; the readiness score reads them. | Label | Code | Meaning | | ------------ | ---------- | -------------------------------------------------- | | **Blocker** | `blocker` | Nothing ships until it is fixed. | | **Critical** | `critical` | A main flow is broken with no way round it. | | **Major** | `major` | A main flow is broken but there is a way round it. | | **Minor** | `minor` | Something is wrong but the flow works. | | **Trivial** | `trivial` | Cosmetic; nobody is blocked. | ## Reproducibility How reliably a bug comes back when its steps are followed, kept on bugs only. | Label | Code | Meaning | | ------------------ | ---------------- | --------------------------------------------------- | | **Always** | `always` | Every time the steps are followed. | | **Sometimes** | `sometimes` | Now and then, under conditions not yet pinned down. | | **Once** | `once` | Seen once; the steps may not bring it back. | | **Not reproduced** | `not_reproduced` | Reported, but nobody has made it happen again. | ## Criterion kind What an [acceptance criterion](/planning/acceptance-criteria) checks. The test generator drafts one case per kind and the quality review flags a story without a negative row. | Label | Code | Meaning | | ------------------ | ---------------- | --------------------------------------------------------------------- | | **Functional** | `functional` | The happy path: what the persona can do. | | **Negative** | `negative` | What must be refused or must not happen. | | **Edge case** | `edge` | The boundary of the input: empty, huge, the last day, the first user. | | **Non-functional** | `non_functional` | A quality the story must keep: speed, access, accessibility. | ## Requirement kind What a [requirement](/planning/functional-requirements) is about. The kind is chosen on a create and fixed after, because each kind numbers on its own sequence and the reference carries the kind's infix. | Label | Code | Meaning | | ------------------ | ---------------- | ---------------------------------------------------------------------------------------- | | **Functional** | `functional` | What the system does for a person: a behaviour that can be shown. `CRM-FR-12`. | | **Non-functional** | `non_functional` | A quality the system keeps while it does it, with a category and a measure. `CRM-NFR-3`. | ## NFR category The quality a [non-functional requirement](/planning/non-functional-requirements) is about. One category per requirement; the standards presets and the readiness score group by it. | Label | Code | Meaning | | ------------------- | ----------------- | --------------------------------------------------------------- | | **Performance** | `performance` | How fast it answers and how much it handles under load. | | **Scalability** | `scalability` | How it grows with more users, data or traffic. | | **Availability** | `availability` | How much of the time it is up, and when it may be down. | | **Reliability** | `reliability` | How rarely it fails and how it recovers when it does. | | **Security** | `security` | How it keeps out who should be out and keeps data safe. | | **Privacy** | `privacy` | How personal data is collected, kept and removed. | | **Usability** | `usability` | How easily a person gets things done with it. | | **Accessibility** | `accessibility` | How people with disabilities use it, and the standard it meets. | | **Compatibility** | `compatibility` | The browsers, devices, systems and versions it works with. | | **Maintainability** | `maintainability` | How easily the team changes it without breaking it. | | **Portability** | `portability` | How it moves between environments, hosts or providers. | | **Compliance** | `compliance` | The laws, regulations and standards it must satisfy. | | **Observability** | `observability` | How its health, errors and behaviour can be seen from outside. | | **Localisation** | `localisation` | The languages, formats, currencies and regions it serves. | | **Other** | `other` | A quality none of the other categories names. | ## Verification method How a requirement is shown to be met. Written with the requirement, so the test module knows which ones get a test case, which are checked by reading, which by calculation and which by walking the client through them. | Label | Code | Meaning | | ----------------- | --------------- | ---------------------------------------------------------------- | | **Test** | `test` | Shown by running a test against it. | | **Inspection** | `inspection` | Shown by examining the code, the configuration or the documents. | | **Analysis** | `analysis` | Shown by calculation, modelling or a review of the data. | | **Demonstration** | `demonstration` | Shown by walking through it in front of the people who asked. | ## Requirement status Where a [requirement](/planning/functional-requirements) or a [business rule](/planning/business-rules) stands, from the first draft to a frozen baseline. In review and approved are reached through the record's [approval](/planning/approvals); baselined is reached when a [baseline](/planning/baselines) that holds the record is frozen and left when an approved [change request](/planning/change-requests) unlocks it; the other moves are made by hand from the list or the flyout, or through the status endpoint. A client guest sees a requirement once it is approved and from then on, through baselined, and loses it when it is deprecated. | Label | Code | Meaning | | -------------- | ------------ | ----------------------------------------------------------------- | | **Draft** | `draft` | Being written; the team reads it, the client does not yet. | | **In review** | `in_review` | Put up for review; waiting for the people who approve it. | | **Approved** | `approved` | Agreed; read by everyone, the client included, and built against. | | **Baselined** | `baselined` | Frozen in a baseline; changed only through a change request. | | **Deprecated** | `deprecated` | Withdrawn; kept for the record and no longer built against. | A draft is submitted for review or deprecated; a review is approved or returned to where it stood by the approvers; an approved requirement is submitted again when it changes, is frozen into baselined by a baseline (or moved there by hand, a badge that carries no lock), or goes to deprecated; a baselined one returns to approved when a change request unlocks it and goes to deprecated only once no frozen baseline holds it; a deprecated one is revived as a draft. ## Business rule kind What a [business rule](/planning/business-rules) is: the six classic kinds of decision a business writes down. The kind shapes how the rule is read, tested and coded, and it may change at any time because every rule numbers on the same sequence. | Label | Code | Meaning | | ------------------ | ---------------- | --------------------------------------------------------------------------------- | | **Constraint** | `constraint` | Something that must or must not be the case: a limit the system refuses to cross. | | **Derivation** | `derivation` | A fact that follows from other facts: how one value is derived from others. | | **Computation** | `computation` | A number worked out from others: the formula, the rounding and the units. | | **Inference** | `inference` | A conclusion drawn when conditions hold: if this is so, then that is true. | | **Action enabler** | `action_enabler` | An action the system takes when a condition holds. | | **Policy** | `policy` | A standing decision of the business, written down so nobody asks again. | ## Use case flow Which flow a step of a [use case](/planning/use-cases) belongs to. The main flow is numbered from one in the order its steps sit; a branch leaves a numbered main step, carries a label such as `2a`, and the stories drafted from a use case come one per flow. | Label | Code | Meaning | | ------------------ | ----------- | ------------------------------------------------------------------- | | **Main flow** | `main` | The path where everything goes right, from the trigger to the goal. | | **Alternate flow** | `alternate` | Another way to the goal that leaves a main step and comes back. | | **Exception flow** | `exception` | A failure that leaves a main step and ends without the goal. | ## RAID kind What an entry of the [RAID log](/planning/raid-log) is. The four kinds share one sequence (`CRM-RSK-4`) and one table, so a kind may change at any time; the kind decides which columns the item carries and how it is scored. | Label | Code | Meaning | | -------------- | ------------ | ------------------------------------------------------------------------------------------ | | **Risk** | `risk` | Something that may happen and would hurt the project; scored by probability times impact. | | **Assumption** | `assumption` | Something taken as true for the plan to hold; reviewed until it is confirmed or fails. | | **Issue** | `issue` | Something that has happened and hurts the project now; scored by impact alone. | | **Dependency** | `dependency` | Something the project needs from outside itself: a team, a vendor, a decision, a delivery. | ## RAID area The part of the project a [RAID item](/planning/raid-log) touches. The area groups the log at the review meeting and filters the table; it never changes how an item is scored or moved. | Label | Code | Meaning | | -------------- | ------------ | --------------------------------------------------------------- | | **Technical** | `technical` | The stack, the architecture, an integration or the data. | | **Schedule** | `schedule` | Dates, sequencing and the time the work takes. | | **Scope** | `scope` | What is in and out, and what keeps growing. | | **Resource** | `resource` | People, skills and availability. | | **Budget** | `budget` | Money: the hours, the licences, the vendors. | | **External** | `external` | A third party, a client decision or something outside the team. | | **Security** | `security` | Access, data exposure and abuse. | | **Compliance** | `compliance` | Law, regulation and the policies the client must honour. | | **Quality** | `quality` | Defects, test coverage and what the client will notice. | | **Other** | `other` | Anything the other areas do not name. | ## RAID status Where a [RAID item](/planning/raid-log) stands. Moved by hand from the list or the flyout, or through the status endpoint; open, monitoring, mitigating and occurred count as needing attention and fill the sidebar badge. | Label | Code | Meaning | | -------------- | ------------ | ----------------------------------------------------------------------------------- | | **Open** | `open` | Raised and not yet looked at. | | **Monitoring** | `monitoring` | Watched at every review; nothing to do yet. | | **Mitigating** | `mitigating` | Being worked on to make it less likely or less bad. | | **Resolved** | `resolved` | Dealt with; the issue is fixed, the dependency delivered, the assumption confirmed. | | **Closed** | `closed` | Off the log; kept for the record. | | **Occurred** | `occurred` | The risk happened; the contingency is now the plan. | | **Accepted** | `accepted` | Lived with on purpose; nobody will act on it. | Every status may move to every other, except that a closed item can only be reopened. ## Decision kind What a [decision](/planning/decisions) is about. The kind says who should sit at the table and where the record is read later; it may change at any time because every decision numbers on the same sequence. | Label | Code | Meaning | | ---------------- | -------------- | ----------------------------------------------------------------------------------------- | | **Architecture** | `architecture` | How the system is built: the stack, the shape of the code, the data, the integrations. | | **Product** | `product` | What the product does and for whom: scope, behaviour and trade-offs the client agrees to. | | **Process** | `process` | How the team works: ceremonies, branching, reviews, releases. | | **Vendor** | `vendor` | Which third party is used and on what terms. | | **Security** | `security` | How access, data and abuse are handled. | ## Decision status Where a [decision](/planning/decisions) stands. Moved by hand from the list or the flyout, or through the status endpoint; accepting a decision that names an earlier one moves that one to superseded in the same breath. | Label | Code | Meaning | | -------------- | ------------ | ----------------------------------------------- | | **Proposed** | `proposed` | Put forward with its options; not yet decided. | | **Accepted** | `accepted` | Decided and in force; the team builds on it. | | **Rejected** | `rejected` | Turned down; kept so the reasoning is not lost. | | **Superseded** | `superseded` | Replaced by a later decision that names it. | | **Deprecated** | `deprecated` | No longer applies and nothing replaced it. | A proposed decision is accepted or rejected; an accepted one is superseded or deprecated; a rejected or deprecated one may be proposed again, and a superseded one accepted again, so the log never needs a duplicate. ## Link kind How one artefact relates to another in the [Links panel](/planning/traceability). A kind is stored once and read from either side: the label is the phrase read from the source, the inverse the phrase read from the target. _Relates to_ and _duplicates_ read the same from both sides, so the reverse direction counts as the same link. | Label | Code | Read from the target | Meaning | | ---------------- | -------------- | -------------------- | ----------------------------------------------------------------------------- | | **implements** | `implements` | is implemented by | The source builds what the target asks for: a story implements a requirement. | | **derives from** | `derives_from` | is refined by | The source was written from the target: a requirement derives from a rule. | | **verifies** | `verifies` | is verified by | The source proves the target holds: a test case verifies a requirement. | | **covers** | `covers` | is covered by | The source walks the target: a test case covers a story or a use case flow. | | **depends on** | `depends_on` | is depended on by | The source cannot finish before the target does. | | **blocks** | `blocks` | is blocked by | The source stops the target from moving until it is done. | | **relates to** | `relates_to` | relates to | The two belong together without one owning the other; the default kind. | | **duplicates** | `duplicates` | duplicates | The two say the same thing; keep one and close the other. | | **is caused by** | `caused_by` | causes | The source happened because of the target: a bug caused by a decision. | | **is fixed by** | `fixed_by` | fixes | The source is put right by the target: a bug fixed by a story. | | **supersedes** | `supersedes` | is superseded by | The source replaces the target, which stays for the record. | ## Attachment kind What kind of file an [attachment](/planning/attachments) is, read from its type and extension when it is stored, so the Files panel shows a preview for a picture and the right badge for anything else. The kind is never picked by hand. | Label | Code | Meaning | | ---------------- | -------------- | -------------------------------------------------------------------- | | **Image** | `image` | A picture: `jpg`, `jpeg`, `png`, `gif`, `webp`; shown as a preview. | | **Document** | `document` | A PDF or a word-processing file: `pdf`, `doc`, `docx`, `odt`, `rtf`. | | **Spreadsheet** | `spreadsheet` | A sheet or a CSV: `xls`, `xlsx`, `ods`, `csv`. | | **Presentation** | `presentation` | Slides: `ppt`, `pptx`, `odp`. | | **Archive** | `archive` | A compressed bundle: `zip`. | | **Text** | `text` | Plain text, Markdown, JSON or a log: `txt`, `md`, `json`, `log`. | | **File** | `other` | Anything else the operator allowed. | ## Quality band What a [quality review](/planning/quality-review)'s score means at a glance. The score is 100 less the penalties of Heimdall's findings; the band reads it for the badge on a list row and the top of the Quality panel. | Label | Code | Meaning | | -------------- | ------------ | -------------------------------------------------------------------------- | | **Ready** | `ready` | 80 to 100: fit for the people after you; the findings, if any, are polish. | | **Needs work** | `needs_work` | 50 to 79: usable with questions; fix the medium and high findings first. | | **Not ready** | `not_ready` | 0 to 49: something would block a developer or a tester; rewrite it. | ## Finding severity How much one finding of a [quality review](/planning/quality-review) matters, and what it takes off the score. | Label | Code | Meaning | | ---------- | -------- | --------------------------------------------------------------------- | | **High** | `high` | A developer or a tester would be blocked or misled; 25 off the score. | | **Medium** | `medium` | The record is usable but would be argued over; 10 off the score. | | **Low** | `low` | Polish: a word, a missing example, a sharper title; 4 off the score. | ## Import status Where a [brief import](/planning/brief-import) stands, from the moment a brief is handed to Heimdall to the last decision on its suggestions. | Label | Code | Meaning | | ------------------- | ----------- | ----------------------------------------------------------------------------------------- | | **Queued** | `queued` | Handed to Heimdall; the reading has not started. | | **Reading** | `running` | Heimdall is reading the brief against what the project holds. | | **Ready to review** | `ready` | The suggestions wait for people. | | **Reviewed** | `accepted` | Every suggestion was decided. | | **Discarded** | `discarded` | A person dropped the batch; its suggestions stay readable but cannot be decided. | | **Failed** | `failed` | The model did not answer, the budget had no room, or the starter is gone; start it again. | ## Research status Where a run of [Heimdall's research](/copilot/research) stands, on the badge beside the **Heimdall suggests** heading of a project's overview and on the run over the API. | Label | Code | Meaning | | --------------- | --------- | ---------------------------------------------------------------------------------------------------------------- | | **Queued** | `queued` | Asked for; the queue has not picked it up. | | **Researching** | `running` | Heimdall is searching and reading; the rail polls until it is done. | | **Ready** | `ready` | The notes are under the run and the suggestions wait on the overview. | | **Failed** | `failed` | The model did not answer, its answer could not be read, the budget had no room, or the asker is gone; ask again. | ## Council session status Where a sitting of [Heimdall's council](/copilot/council) stands, on the Council tab of the Heimdall panel and on the sitting over the API; a voice's turn passes through the same words without the summarising step. | Label | Code | Meaning | | --------------- | ------------- | --------------------------------------------------------------------------------------------------------------- | | **Queued** | `queued` | Convened; no voice has begun. | | **In session** | `running` | The voices are reading and speaking; the tab refreshes as each finishes. | | **Summarising** | `summarising` | Every voice has spoken; Heimdall is writing the verdict. | | **Ready** | `ready` | The verdict is written and the findings wait for decisions. | | **Failed** | `failed` | No voice could answer, or the moderator could not; the reason is on the tab, and the council is convened again. | ## Council finding status What a person decided about one finding of the council. A decision is never undone. | Label | Code | Meaning | | ------------- | ----------- | ---------------------------------------------------------------------------------------------------------- | | **Waiting** | `pending` | Waiting for a decision. | | **Accepted** | `accepted` | On the review queue as a suggestion carrying the voice; the record the voice drafted was written in draft. | | **Dismissed** | `dismissed` | Nothing written; the idea is remembered so no voice and no research run raises it again. | ## Suggestion status What a person decided about one suggestion of a [brief import](/planning/brief-import) or of [Heimdall's research](/copilot/research). A decision is never undone: the record it wrote has its own lifecycle. | Label | Code | Meaning | | ----------------------- | ----------- | ------------------------------------------------------------------ | | **Pending** | `pending` | Waiting for a decision. | | **Accepted** | `accepted` | The record was written as Heimdall proposed; a note was kept. | | **Accepted with edits** | `edited` | The record was written with the person's changes. | | **Merged** | `merged` | Pointed at a record the project already held; nothing was written. | | **Rejected** | `rejected` | Dropped; nothing was written. | | **Dismissed** | `dismissed` | A note closed without a record. | ## Suggestion kind What a suggestion from a [brief import](/planning/brief-import) proposes to add. Twelve kinds become a record when accepted; the two notes are read and kept or dismissed. The review queue groups suggestions in this order. | Label | Code | Becomes | | ------------------------------ | ---------------------------- | ---------------------------------------------- | | **Epic** | `epic` | An epic. | | **Story** | `story` | A story on the backlog, under its epic. | | **Functional requirement** | `functional_requirement` | A functional requirement. | | **Non-functional requirement** | `non_functional_requirement` | A non-functional requirement with its measure. | | **Business rule** | `business_rule` | A business rule with its examples. | | **Use case** | `use_case` | A use case with its main flow. | | **Glossary term** | `glossary_term` | A glossary term with its aliases. | | **Persona** | `persona` | A persona. | | **Stakeholder** | `stakeholder` | A stakeholder. | | **Risk** | `risk` | A risk on the RAID log, scored. | | **Assumption** | `assumption` | An assumption on the RAID log. | | **Decision** | `decision` | A proposed decision. | | **Improvement** | `improvement` | Nothing: a gap or a question for the analyst. | | **Research** | `research` | Nothing: a finding of the research sweep. | ## Revision reason Why a [version](/planning/revisions-and-versions) of an artefact was written. Every save of a revisable artefact writes one; the reason says what kind of save it was, so the history reads as a story rather than a list of diffs. Beside the reason, each version records through what it was written: a person in the dashboard, an API token, an MCP client, an integration, a webhook, Heimdall or the system. | Label | Code | Meaning | | ----------------------- | ------------ | ----------------------------------------------------- | | **Created** | `created` | The first version, as the artefact was written. | | **Edited** | `edit` | A person changed the fields by hand. | | **Drafted by Heimdall** | `ai_assist` | Heimdall drafted the change and a person accepted it. | | **Imported** | `import` | The change arrived from a brief import. | | **Moved** | `transition` | The artefact moved along its lifecycle. | | **Restored** | `restore` | An earlier version was put back. | | **Approved** | `approval` | The version an approval froze. | | **Baselined** | `baseline` | The version a baseline froze. | | **Saved as a version** | `manual` | A person saved a version on purpose. | ## Baseline kind What a [baseline](/planning/baselines) marks. The kind is chosen when the draft is composed and decides who signs: a scope baseline passes the client's sign-off after the team's approval; the other kinds are frozen by the team's approval alone. Shown as a badge in the list and the flyout; a filter on the list takes the code. | Label | Code | Meaning | | ---------------------- | ----------- | ----------------------------------------------------------------------------------- | | **Scope baseline** | `scope` | The agreed scope of a piece of work; frozen by the team, then signed by the client. | | **Release baseline** | `release` | What a release ships, frozen when its scope is cut. | | **Sprint baseline** | `sprint` | What a sprint commits to, frozen when the sprint starts. | | **Milestone baseline** | `milestone` | What was promised for a milestone, frozen when it is agreed. | ## Baseline status Where a [baseline](/planning/baselines) stands. Every move but the last is the approval engine's: submitting a draft puts it in review, the project manager's approval freezes it, the second submission of a scope baseline sends it to the client, and their signature signs it; superseding is the one move made by hand. A client guest sees a baseline from the moment it is frozen. | Label | Code | Meaning | | --------------------- | ------------------- | ------------------------------------------------------------------------------------ | | **Draft** | `draft` | Being composed; records can still be added and removed. | | **In review** | `in_review` | Submitted to the project manager; nothing changes until the verdict. | | **Approved** | `approved` | Frozen by the team; its records are locked and change only through a change request. | | **Awaiting sign-off** | `awaiting_sign_off` | Frozen and sent to the client's stakeholders for their signature. | | **Signed off** | `signed_off` | Signed by the client; the record the project is held to. | | **Superseded** | `superseded` | Replaced by a newer baseline; kept for the record and no longer locking anything. | A draft goes to in review; a review is approved or returned to draft; an approved scope baseline goes to awaiting sign-off, and any approved baseline to superseded; a baseline awaiting sign-off is signed or returned to approved; a signed baseline goes to superseded. ## Change reason Why a [change request](/planning/change-requests) was raised. Chosen when the request is raised and changed until it is decided; shown as a badge and a filter on the list. | Label | Code | Meaning | | ------------------- | ----------------- | -------------------------------------------------------------------- | | **New requirement** | `new_requirement` | Something the agreed record never covered. | | **Clarification** | `clarification` | The record says it, but not clearly enough to build or test against. | | **Defect** | `defect` | The record as agreed is wrong and must change, not merely the code. | | **Regulatory** | `regulatory` | A law, a standard or a contract requires the change. | | **Client request** | `client_request` | The client asked for it after the record was agreed. | | **Technical debt** | `technical_debt` | The team asks to change the record to repay a shortcut. | | **Scope reduction** | `scope_reduction` | Something agreed is dropped. | ## Change request status Where a [change request](/planning/change-requests) stands. The moves into and out of the gate (awaiting approval, approved, rejected) are the approval engine's; the rest are made by hand from the list or the flyout, or through the status endpoint, and Heimdall's reading moves a request that was only submitted under analysis. A request is edited while submitted, under analysis or awaiting approval, and deleted while submitted, under analysis or rejected; the open statuses, everything but rejected, verified and closed, are what the sidebar badge counts. | Label | Code | Meaning | | --------------------- | ------------------- | --------------------------------------------------------------------------- | | **Submitted** | `submitted` | Raised and waiting for an analyst to pick it up. | | **Under analysis** | `under_analysis` | The impact on scope, schedule, effort and risk is being worked out. | | **Awaiting approval** | `awaiting_approval` | Sent to the approvers with its impact; nothing moves until they answer. | | **Approved** | `approved` | Agreed; the baseline items it names are unlocked for the change. | | **Rejected** | `rejected` | Refused by the approvers, with their reason; the baseline stands as it was. | | **Implementing** | `implementing` | The team is making the change. | | **Implemented** | `implemented` | The change is made and waits to be verified. | | **Verified** | `verified` | Confirmed to have landed as asked, by whom and when. | | **Closed** | `closed` | Finished, or withdrawn; kept for the record. | A submitted request goes under analysis, to the approvers or to closed; one under analysis goes to the approvers or to closed; one awaiting approval is approved, rejected or returned under analysis by the approvers; an approved one goes to implementing or closed; a rejected one back under analysis or to closed; implementing to implemented, implemented to verified (or back to implementing), verified to closed. ## Impact level How hard a [change request](/planning/change-requests) hits the project: the word the approvers read first, written by the analyst or proposed by Heimdall's reading. Shown as a badge in the list and the flyout; a filter on the list takes the code. | Label | Code | Meaning | | ---------- | -------- | ------------------------------------------------------------------------------ | | **Low** | `low` | Absorbed; no date and no price moves. | | **Medium** | `medium` | A date or the effort moves, but the plan is reworked rather than renegotiated. | | **High** | `high` | The scope, the schedule or the budget needs the client's agreement again. | ## Document status Where a [generated document](/documents/generated-documents) stands. The render moves it first: asked for, it is rendering; the file written, it is a draft; a render that stopped leaves it failed with the reason kept. The approval engine moves a draft into review and, when the request carries, to final, superseding the kind's earlier final copy; superseding by hand is the one move a person makes. A client guest sees a document from the moment it is final. | Label | Code | Meaning | | -------------- | ------------ | ---------------------------------------------------------------------------------------------------------------- | | **Rendering** | `rendering` | Asked for; the file is being written in the background and the list refreshes itself until it is there. | | **Draft** | `draft` | Rendered and readable; the title, the scope and the opening page still change, and each change renders it again. | | **Failed** | `failed` | The render stopped; the reason is on the document, and Render Again clears it. | | **In review** | `in_review` | Submitted to the project manager; nothing changes until the verdict. | | **Final** | `final` | Approved: the copy the client sees, never edited or rendered again. | | **Superseded** | `superseded` | Replaced by a newer final copy of its kind, or retired by hand; kept for the record, unchanged. | Rendering goes to draft or failed; a draft or a failed document renders again, back through rendering; a draft goes to in review; a review is approved to final or returned to draft; a final copy goes to superseded, by the next approval of its kind or by hand. ## Sprint status Where a [sprint](/building/sprints) stands. Planning writes it planned; putting it to the team moves it to awaiting commitment, and the team's approval starts it; the close completes it; cancelling is the one move made by hand. A client guest never reads sprints. | Label | Code | Meaning | | ----------------------- | ------------ | ----------------------------------------------------------------------------------- | | **Planned** | `planned` | Being planned; its dates, capacity and scope still change. | | **Awaiting commitment** | `committing` | Put to the team for its commitment; the scope holds until they answer. | | **Active** | `active` | Running: the team committed to its scope and the board follows it. One per project. | | **Completed** | `completed` | Closed with its velocity recorded; the unfinished work was carried on. | | **Cancelled** | `cancelled` | Called off before or during the run; its items went back to the backlog. | A planned sprint goes to awaiting commitment or cancelled; one awaiting commitment is started to active or returned to planned; an active sprint is closed to completed or cancelled; a completed or cancelled sprint goes nowhere. ## Approval status Where an [approval request](/planning/approvals) stands. Submitting opens it pending; the verdicts of the people named, the author's withdrawal or the policy's clock settle it. Shown on the approval bar of the record and in the inbox; a filter on the inbox takes the code. | Label | Code | Meaning | | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | **Pending** | `pending` | Waiting for the approvers named on it; the record is in review. | | **Approved** | `approved` | Carried: enough approvers agreed, or the clock ran out on a policy that approves by itself; the record is approved at a new version. | | **Rejected** | `rejected` | One approver rejected it; closed, and the record went back to where it stood. | | **Changes requested** | `changes_requested` | An approver asked for changes; held open for the author to edit the record and resubmit the next round. | | **Withdrawn** | `withdrawn` | Taken back by the author or a person with `approval:withdraw`; the record went back to where it stood. | | **Expired** | `expired` | Reserved for a request closed because its time ran out unanswered; no policy closes one this way yet. | ## Approval verdict What one approver said on one round of a request, kept with the time, the address and a signature hash; the author reads it beside the approver's name and the reason. | Label | Code | Meaning | | --------------------- | ------------------- | ---------------------------------------------------------------------------------------------------- | | **Approved** | `approved` | Counts towards the approvals the policy needs; the record is approved once enough approvers agree. | | **Rejected** | `rejected` | Closes the request under every rule and returns the record to where it stood. | | **Changes requested** | `changes_requested` | Holds the request for the author to edit and resubmit; the approvers decide again on the next round. | ## Related - [Webhooks](/workspace/webhooks) — the delivery log and retries - [Heimdall panel](/copilot/panel) — cards and the work timeline - [Error codes](/api/v1/errors) — the codes an API refusal carries --- # ai.budget_exhausted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai/ai-budget-exhausted The month's spend reaches the cap. ### When this fires A metered call takes the month's spend to the cap. From then on every assist, Heimdall panel, the brief import and the scheduled digests refuse with `AI_BUDGET_EXHAUSTED` until an admin raises the cap or the month turns. ### Related events - `ai.budget_warning`: predecessor. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `ai.budget_exhausted` | yes | The event name, always `ai.budget_exhausted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.cap_usd` | number | yes | The month's budget, in US dollars. | | `data.month` | string | yes | The month the budget is for, `YYYY-MM`. | | `data.percent` | integer | yes | Always 100 here. | | `data.project_id` | string \| null (uuid) | yes | The project the budget is for; null for the team-wide budget. | | `data.spent_usd` | number | yes | The month's spend, now equal to the cap. | | `data.team_id` | string (uuid) | yes | The team. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VA1", "type": "ai.budget_exhausted", "created_at": "2026-11-28T09:12:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "cap_usd": 40, "month": "2026-11", "percent": 100, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "spent_usd": 40, "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # ai.budget_warning Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai/ai-budget-warning The month's spend crosses the budget's alert threshold. ### When this fires A metered call takes the month's spend past the budget's alert percentage. The assists keep working; the event, the in-app alert and the Telegram message are the nudge to raise the cap or slow down. ### Related events - `ai.budget_exhausted`: what follows if nothing changes. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `ai.budget_warning` | yes | The event name, always `ai.budget_warning` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.cap_usd` | number | yes | The month's budget, in US dollars. | | `data.month` | string | yes | The month the budget is for, `YYYY-MM`. | | `data.percent` | integer | yes | The spend as a whole percentage of the cap at the moment the event fired. | | `data.project_id` | string \| null (uuid) | yes | The project the budget is for; null for the team-wide budget. | | `data.spent_usd` | number | yes | The month's spend so far, in US dollars. | | `data.team_id` | string (uuid) | yes | The team. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VA0", "type": "ai.budget_warning", "created_at": "2026-11-24T13:40:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "cap_usd": 100, "month": "2026-11", "percent": 81, "project_id": null, "spent_usd": 81.37, "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # ai.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai The team's monthly AI budget approaching and reaching its cap. Every Heimdall turn, inline assist, brief import and quality review is metered against a budget an admin sets under **Settings → AI**, per team or per project; these two events let a finance mirror or a chat channel hear about it before the assists switch off. Each event fires once per budget per month: the warning when the threshold is first crossed and the exhaustion when the cap is first reached. A project budget carries its `project_id`; a team-wide budget carries `null`. > **Arriving with Heimdall.** The budgets, the meter and these two events land with Heimdall shell; the shape is fixed now so a subscriber can be built ahead of it. ## Required ability Tokens subscribing to `ai.*` events need `ai:manage-budgets` at mint time, the permission that sets the caps. ## Events - [`ai.budget_warning`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai/ai-budget-warning) — The month's spend crosses the budget's alert threshold. - [`ai.budget_exhausted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai/ai-budget-exhausted) — The month's spend reaches the cap. --- # approval.approved Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-approved A request carries. ### When this fires The approvals given this round reach what the policy's rule needs (one, all, or the quorum), or the policy had the gate switched off, or an unanswered request passed the time after which the policy lets it carry by itself (`auto` is true for the last two). The artefact is stamped approved at the next version, bound to the revision that was submitted; its own family announces the status move beside this event. ### Related events - `requirement.status_changed`, `epic.status_changed`, `business_rule.status_changed`, `use_case.status_changed`: the move into approved on the record itself. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `approval.approved` | yes | The event name, always `approval.approved` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.approvable_id` | string (uuid) | yes | The artefact approved. | | `data.approvable_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The artefact's family. | | `data.approval_id` | string (uuid) | yes | The request. | | `data.auto` | boolean | yes | True when the gate was off or the policy let the request carry by itself. | | `data.kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | yes | The gate. | | `data.project_id` | string (uuid) | yes | The project. | | `data.reference` | string | yes | The artefact's key. | | `data.version` | integer \| null | yes | The artefact's approved version after this approval; null only when the artefact is gone. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VP2", "type": "approval.approved", "created_at": "2026-10-28T11:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "approvable_type": "requirement", "approval_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "auto": false, "kind": "requirement_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-FR-12", "version": 1 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # approval.changes_requested Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-changes-requested An approver asks for changes. ### When this fires A verdict of `changes_requested` holds the request open: no further verdict is taken until the author edits the record and resubmits it, which opens the next round with the approvers resolved again. ### Related events - `approval.requested`: the resubmission, as the next round. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `approval.changes_requested` | yes | The event name, always `approval.changes_requested` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.approvable_id` | string (uuid) | yes | The artefact. | | `data.approvable_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The artefact's family. | | `data.approval_id` | string (uuid) | yes | The request. | | `data.kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | yes | The gate. | | `data.project_id` | string (uuid) | yes | The project. | | `data.reference` | string | yes | The artefact's key. | | `data.round` | integer | yes | The round that was held for changes. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VP4", "type": "approval.changes_requested", "created_at": "2026-10-28T11:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "approvable_type": "requirement", "approval_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "kind": "requirement_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-FR-12", "round": 1 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # approval.decided Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-decided One approver gives a verdict. ### When this fires An approver named on a pending request approves it, rejects it or asks for changes, on the approvals inbox, the record's approval bar or the decide endpoint. The verdict is written once per person per round with a signature hash; the event that follows says what it settled. ### Related events - `approval.approved`: enough approvers agreed. - `approval.rejected`, `approval.changes_requested`: the verdict closed or held the request. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `approval.decided` | yes | The event name, always `approval.decided` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.approvable_id` | string (uuid) | yes | The artefact under approval. | | `data.approvable_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The artefact's family. | | `data.approval_id` | string (uuid) | yes | The request. | | `data.approver_id` | string \| null (uuid) | yes | The person who decided. | | `data.comment` | string \| null | yes | The reason they gave; null when none. | | `data.kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | yes | The gate. | | `data.project_id` | string (uuid) | yes | The project. | | `data.reference` | string | yes | The artefact's key. | | `data.round` | integer | yes | The round the verdict answers. | | `data.verdict` | `approved`, `rejected`, `changes_requested` | yes | What the approver said. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VP1", "type": "approval.decided", "created_at": "2026-10-28T11:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "approvable_type": "requirement", "approval_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "approver_id": "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b", "comment": null, "kind": "requirement_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-FR-12", "round": 1, "verdict": "approved" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # approval.escalated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-escalated A pending request passes the time the policy allows. ### When this fires The hourly sweep finds a pending request past its `due_at` that has not been escalated yet, and tells the project's lead (or the team's owner when the project names none). It fires once per round; the request stays pending and the approvers may still decide it. ### Related events - `approval.decided`: the answer that eventually comes. - `approval.approved`: a request the policy lets carry by itself, with `auto` true. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `approval.escalated` | yes | The event name, always `approval.escalated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.approvable_id` | string (uuid) | yes | The artefact. | | `data.approvable_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The artefact's family. | | `data.approval_id` | string (uuid) | yes | The request. | | `data.due_at` | string \| null (date-time) | yes | When the approvers' time ran out. | | `data.escalated_to` | string (uuid) | yes | The person told: the project's lead or the team owner. | | `data.kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | yes | The gate. | | `data.project_id` | string (uuid) | yes | The project. | | `data.reference` | string | yes | The artefact's key. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VP6", "type": "approval.escalated", "created_at": "2026-10-31T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "approvable_type": "requirement", "approval_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "due_at": "2026-10-31T09:20:00+00:00", "escalated_to": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e", "kind": "requirement_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-FR-12" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # approval.policy_updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-policy-updated A project changes how one kind of approval is decided. ### When this fires Someone with `approval:manage-policies` saves a policy on the project settings page or through the policy endpoint and something differs from the policy that applied before, defaults included. Requests already open keep the rule and the approvers they were submitted with. ### Related events - `approval.requested`: the next request opens under the new policy. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `approval.policy_updated` | yes | The event name, always `approval.policy_updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | Each changed field with `from` and `to`, against the policy that applied before. | | `data.kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | yes | The gate the policy decides. | | `data.project_id` | string (uuid) | yes | The project. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VP7", "type": "approval.policy_updated", "created_at": "2026-10-28T08:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "rule": { "from": "any", "to": "quorum" }, "quorum": { "from": 1, "to": 2 } }, "kind": "requirement_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # approval.rejected Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-rejected An approver rejects the request. ### When this fires One verdict of `rejected` closes the request under every rule, with the reason the policy required. The artefact returns to where it stood before it was submitted: draft when it was never approved, approved at its last version when it was. ### Related events - `approval.decided`: the verdict itself, with its reason. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `approval.rejected` | yes | The event name, always `approval.rejected` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.approvable_id` | string (uuid) | yes | The artefact. | | `data.approvable_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The artefact's family. | | `data.approval_id` | string (uuid) | yes | The request. | | `data.kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | yes | The gate. | | `data.project_id` | string (uuid) | yes | The project. | | `data.reference` | string | yes | The artefact's key. | | `data.round` | integer | yes | The round the rejection closed. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VP3", "type": "approval.rejected", "created_at": "2026-10-28T11:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "approvable_type": "requirement", "approval_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "kind": "requirement_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-FR-12", "round": 1 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # approval.requested Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-requested An artefact is put up for approval. ### When this fires A person submits an epic, a story, a requirement, a business rule or a use case for review on its page, an agent calls `submit_for_review`, or a token posts to the artefact's submit endpoint; also when the author resubmits after changes were requested, with the next `round`. The approvers were resolved from the project's policy at that moment and the request is bound to the version of the record as submitted. ### Related events - `approval.decided`: each approver's answer. - `approval.approved`, `approval.rejected`, `approval.changes_requested`: what the answers settle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `approval.requested` | yes | The event name, always `approval.requested` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.approvable_id` | string (uuid) | yes | The artefact under approval. | | `data.approvable_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The artefact's family. | | `data.approval_id` | string (uuid) | yes | The request. | | `data.approver_ids` | array of string (uuid) | yes | The people asked, as the policy resolved them. | | `data.due_at` | string \| null (date-time) | yes | When the approvers' time runs out; null when the policy sets no clock. | | `data.kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | yes | The gate. | | `data.project_id` | string (uuid) | yes | The project. | | `data.reference` | string | yes | The artefact's key, such as `CRM-FR-12`. | | `data.round` | integer | yes | 1 for a first submission; one more for each resubmission after changes. | | `data.title` | string | yes | The artefact's title as submitted. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VP0", "type": "approval.requested", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "approvable_type": "requirement", "approval_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "approver_ids": [ "3f8a1c2e-7b4d-4e5f-9a6b-1c2d3e4f5a6b" ], "due_at": "2026-10-31T09:20:00+00:00", "kind": "requirement_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-FR-12", "round": 1, "title": "Apply a voucher at checkout" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # approval.withdrawn Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-withdrawn An open request is taken back. ### When this fires The person who raised the request, or someone whose role carries `approval:withdraw`, withdraws it while it is pending or waiting for changes. The artefact returns to where it stood; the approvers' queues simply lose the request. ### Related events - `approval.requested`: a later submission is a new request. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `approval.withdrawn` | yes | The event name, always `approval.withdrawn` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.approvable_id` | string (uuid) | yes | The artefact. | | `data.approvable_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case` | yes | The artefact's family. | | `data.approval_id` | string (uuid) | yes | The request. | | `data.kind` | `requirement_review`, `client_sign_off`, `test_plan_approval`, `baseline_approval`, `change_request`, `deployment_promotion`, `release_go_live`, `post_mortem_sign_off`, `document_approval`, `timesheet_approval`, `sprint_commitment` | yes | The gate. | | `data.project_id` | string (uuid) | yes | The project. | | `data.reference` | string | yes | The artefact's key. | | `data.round` | integer | yes | The round that was open. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VP5", "type": "approval.withdrawn", "created_at": "2026-10-28T12:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "approvable_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "approvable_type": "requirement", "approval_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "kind": "requirement_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-FR-12", "round": 1 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # approval.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval The life of an approval request: an artefact put up for approval, each approver's verdict, the outcome the verdicts settle, a request taken back, a request that ran past its time, and a change to how a project decides a kind of approval. A client dashboard that shows what waits for sign-off, a Slack or Teams relay that tells approvers, and an audit trail of who approved what subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's approvals. The payloads carry the ids, the artefact's reference and what the move itself carries, never the artefact or the verdicts in full: read the [approvals endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/approvals) for the request with its decisions, and the artefact's own family (`requirement.status_changed` and its siblings) for the status move an approval makes on the record. ## Required ability Tokens subscribing to `approval.*` events need `approval:view` at mint time. ## Events - [`approval.requested`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-requested) — An artefact is put up for approval. - [`approval.decided`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-decided) — One approver gives a verdict. - [`approval.approved`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-approved) — A request carries. - [`approval.rejected`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-rejected) — An approver rejects the request. - [`approval.changes_requested`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-changes-requested) — An approver asks for changes. - [`approval.withdrawn`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-withdrawn) — An open request is taken back. - [`approval.escalated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-escalated) — A pending request passes the time the policy allows. - [`approval.policy_updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-policy-updated) — A project changes how one kind of approval is decided. --- # attachment.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment/attachment-created A file is uploaded onto an artefact. ### When this fires A person uploads a file from an artefact's flyout, or a token posts one through the API. The payload carries the artefact, the file's name, size and kind, and who uploaded it. ### Related events - `attachment.deleted`: the end of the file. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `attachment.created` | yes | The event name, always `attachment.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.artefact_id` | string (uuid) | yes | The artefact the file is pinned on. | | `data.artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The artefact's type. | | `data.attachment_id` | string (uuid) | yes | The attachment's id. | | `data.kind` | `image`, `document`, `spreadsheet`, `presentation`, `archive`, `text`, `other` | yes | What kind of file it is. | | `data.name` | string | yes | The file's name as uploaded. | | `data.project_id` | string (uuid) | yes | The project the artefact belongs to. | | `data.size` | integer | yes | The file's size in bytes. | | `data.uploaded_by` | string \| null (uuid) | yes | The person who uploaded it; null when the account is gone. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VKA", "type": "attachment.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "artefact_type": "work-item", "attachment_id": "c9d0e1f2-a3b4-4c5d-9e6f-7a8b9c0d1e23", "kind": "image", "name": "checkout-wireframe.png", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "size": 184320, "uploaded_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # attachment.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment/attachment-deleted A file is removed. ### When this fires The uploader or someone with `attachment:delete` removes a file from the artefact's flyout, or a token deletes it through the API. The bytes are gone with the record; the payload carries what the file was, so a mirror drops its copy. ### Related events - `attachment.created`: the file's beginning. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `attachment.deleted` | yes | The event name, always `attachment.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.artefact_id` | string (uuid) | yes | The artefact the file was pinned on. | | `data.artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The artefact's type. | | `data.attachment_id` | string (uuid) | yes | The attachment's id. | | `data.kind` | `image`, `document`, `spreadsheet`, `presentation`, `archive`, `text`, `other` | yes | What kind of file it was. | | `data.name` | string | yes | The file's name as uploaded. | | `data.project_id` | string (uuid) | yes | The project the artefact belongs to. | | `data.size` | integer | yes | The file's size in bytes. | | `data.uploaded_by` | string \| null (uuid) | yes | The person who uploaded it; null when the account is gone. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VKB", "type": "attachment.deleted", "created_at": "2026-11-02T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "artefact_type": "work-item", "attachment_id": "c9d0e1f2-a3b4-4c5d-9e6f-7a8b9c0d1e23", "kind": "image", "name": "checkout-wireframe.png", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "size": 184320, "uploaded_by": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # attachment.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment The files pinned on a project's artefacts: a file uploaded and a file removed. A document store that mirrors the project's files and a scanner that checks each upload subscribe to this family; the bytes are fetched over the API with a token that may read them. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads carry the artefact, the file's name, size and kind and who uploaded it, never the bytes or a URL: read the [attachments endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/attachments) for the download endpoint. ## Required ability Tokens subscribing to `attachment.*` events need `attachment:view` at mint time. ## Events - [`attachment.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment/attachment-created) — A file is uploaded onto an artefact. - [`attachment.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment/attachment-deleted) — A file is removed. --- # baseline.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-created A baseline is added. ### When this fires A person adds a baseline on the Baselines page, Heimdall or an agent calls `create_baseline`, or a token posts to the baselines endpoint. The baseline starts as an empty draft; its records are set afterwards. ### Related events - `baseline.updated`: the records joining it. - `baseline.frozen`: the moment it stops changing. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `baseline.created` | yes | The event name, always `baseline.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.baseline_id` | string (uuid) | yes | The baseline's id. | | `data.kind` | `scope`, `release`, `sprint`, `milestone` | yes | The kind. | | `data.name` | string | yes | What the baseline is called. | | `data.project_id` | string (uuid) | yes | The project the baseline belongs to. | | `data.status` | `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off`, `superseded` | yes | Where it starts, which is always `draft`. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ0", "type": "baseline.created", "created_at": "2026-11-03T08:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "baseline_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "kind": "scope", "name": "v1.0 scope", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "status": "draft" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # baseline.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-deleted A draft baseline is deleted. ### When this fires A person deletes the draft from the Baselines page, Heimdall or an agent calls `delete_baseline`, or a token calls the delete endpoint. The row, its items and its versions are gone; the payload carries what a mirror needs to drop its own copy. ### Related events - `baseline.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `baseline.deleted` | yes | The event name, always `baseline.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.baseline_id` | string (uuid) | yes | The baseline's id. | | `data.kind` | `scope`, `release`, `sprint`, `milestone` | yes | The kind it was. | | `data.name` | string | yes | What the baseline was called. | | `data.project_id` | string (uuid) | yes | The project the baseline belonged to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ6", "type": "baseline.deleted", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "baseline_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "kind": "scope", "name": "v1.0 scope", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # baseline.frozen Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-frozen The team's approval froze a baseline. ### When this fires The `baseline_approval` request on the baseline carries, by the approvers' verdicts or by the policy itself. Every item is pinned to its approved version and locked, the requirements and rules among them move to baselined, and from now on a change to anything the baseline holds needs an approved change request. ### Related events - `baseline.signed_off`: the client's signature on a scope baseline. - `change_request.status_changed`: a request approved against it unlocks items. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `baseline.frozen` | yes | The event name, always `baseline.frozen` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.approval_id` | string (uuid) | yes | The approval request that carried. | | `data.baseline_id` | string (uuid) | yes | The baseline's id. | | `data.item_count` | integer | yes | How many records were frozen. | | `data.kind` | `scope`, `release`, `sprint`, `milestone` | yes | The kind; a scope baseline now awaits the client. | | `data.name` | string | yes | What the baseline is called. | | `data.project_id` | string (uuid) | yes | The project the baseline belongs to. | | `data.version` | integer | yes | The baseline's approved version, which is 1. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ3", "type": "baseline.frozen", "created_at": "2026-11-10T09:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "approval_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "baseline_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "item_count": 2, "kind": "scope", "name": "v1.0 scope", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "version": 1 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # baseline.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-restored An earlier version of a draft baseline is put back. ### When this fires A person restores a version from the baseline's history, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, items included, and the payload carries both numbers. A frozen baseline is never restored over. ### Related events - `baseline.updated`: an ordinary change to the draft. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `baseline.restored` | yes | The event name, always `baseline.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.baseline_id` | string (uuid) | yes | The baseline's id. | | `data.name` | string | yes | What the baseline is called after the restore. | | `data.project_id` | string (uuid) | yes | The project the baseline belongs to. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ5", "type": "baseline.restored", "created_at": "2026-11-05T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "baseline_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "name": "v1.0 scope", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "restored_from_version": 2, "version": 4 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # baseline.signed_off Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-signed-off The client signed a scope baseline. ### When this fires The `client_sign_off` request on a scope baseline carries: every stakeholder who approves requirements, with an account linked to their seat, approved it. The baseline is the record the project is held to from now on. ### Related events - `baseline.frozen`: the team's approval that came first. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `baseline.signed_off` | yes | The event name, always `baseline.signed_off` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.approval_id` | string (uuid) | yes | The sign-off request that carried. | | `data.baseline_id` | string (uuid) | yes | The baseline's id. | | `data.name` | string | yes | What the baseline is called. | | `data.project_id` | string (uuid) | yes | The project the baseline belongs to. | | `data.version` | integer | yes | The baseline's approved version, which is now 2. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ4", "type": "baseline.signed_off", "created_at": "2026-11-12T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "approval_id": "d2e3f4a5-b6c7-4d8e-9f0a-1b2c3d4e5f60", "baseline_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "name": "v1.0 scope", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "version": 2 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # baseline.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-status-changed A baseline moves between its states. ### When this fires The approval engine moves a baseline into review, approved, awaiting sign-off or signed off, a rejection or a withdrawal returns it, or a person supersedes it by hand. The payload carries the status before and after; `reference` repeats the name, which is the key people use for a baseline. ### Related events - `baseline.frozen`: fires beside the move into approved. - `baseline.signed_off`: fires beside the move into signed off. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `baseline.status_changed` | yes | The event name, always `baseline.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.baseline_id` | string (uuid) | yes | The baseline's id. | | `data.name` | string | yes | What the baseline is called. | | `data.previous_status` | `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off`, `superseded` | yes | Where the baseline was. | | `data.project_id` | string (uuid) | yes | The project the baseline belongs to. | | `data.reference` | string | yes | The name again, as the key. | | `data.status` | `draft`, `in_review`, `approved`, `awaiting_sign_off`, `signed_off`, `superseded` | yes | Where the baseline is now. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ2", "type": "baseline.status_changed", "created_at": "2026-11-10T09:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "baseline_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "name": "v1.0 scope", "previous_status": "in_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "v1.0 scope", "status": "approved" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # baseline.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-updated A draft baseline's words or records change. ### When this fires The name, the kind or the description of a draft changes, or the records it holds are set. `changes` holds one entry per column that changed, each with the value before and after; when the records changed, `changes.items` carries the references held before and after. ### Related events - `baseline.status_changed`: the move that is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `baseline.updated` | yes | The event name, always `baseline.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.baseline_id` | string (uuid) | yes | The baseline's id. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`; `items` lists references before and after. | | `data.name` | string | yes | What the baseline is called after the change. | | `data.project_id` | string (uuid) | yes | The project the baseline belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ1", "type": "baseline.updated", "created_at": "2026-11-04T09:10:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "baseline_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "changes": { "items": { "from": [ "CRM-FR-12" ], "to": [ "CRM-FR-12", "CRM-142" ] } }, "name": "v1.0 scope", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # baseline.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline The life of a project's baselines: a baseline added as a draft, its words or items changed, moved by hand, frozen by the team's approval, signed by the client, put back to an earlier version and deleted. A client dashboard that shows what was signed, a release tool that reads the scope a version is cut from, and an audit trail of what the project is held to subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the name, the kind and what the move itself carries, never the items. Read the [baselines endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/baselines) for the record with its items and versions, and the [`approval.*` family](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval) for the request behind a freeze or a signature. ## Required ability Tokens subscribing to `baseline.*` events need `baseline:view` at mint time. ## Events - [`baseline.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-created) — A baseline is added. - [`baseline.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-updated) — A draft baseline's words or records change. - [`baseline.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-status-changed) — A baseline moves between its states. - [`baseline.frozen`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-frozen) — The team's approval froze a baseline. - [`baseline.signed_off`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-signed-off) — The client signed a scope baseline. - [`baseline.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-restored) — An earlier version of a draft baseline is put back. - [`baseline.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-deleted) — A draft baseline is deleted. --- # business_rule.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-created A rule is added. ### When this fires A person adds a rule on the Business Rules page, an agent proposes one, a brief import is accepted, or a token creates one through the API. The payload carries the reference the rule was numbered with, its kind, the status it starts in and its priority. ### Related events - `business_rule.updated`: the next change. - `business_rule.status_changed`: the rule's review lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `business_rule.created` | yes | The event name, always `business_rule.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.business_rule_id` | string (uuid) | yes | The rule's id. | | `data.name` | string | yes | What the rule is called. | | `data.priority` | `must`, `should`, `could`, `wont` | yes | Its MoSCoW priority. | | `data.project_id` | string (uuid) | yes | The project the rule belongs to. | | `data.reference` | string | yes | The key it was numbered with, such as `CRM-BR-7`. | | `data.status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | yes | Where it starts: `draft` unless the creator said otherwise. | | `data.type` | `constraint`, `derivation`, `computation`, `inference`, `action_enabler`, `policy` | yes | The kind. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VF0", "type": "business_rule.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "business_rule_id": "d5e6f7a8-b9c0-4d1e-8f2a-3b4c5d6e7f80", "name": "Platinum tier threshold", "priority": "must", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-BR-7", "status": "draft", "type": "inference" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # business_rule.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-deleted A rule is deleted. ### When this fires A person deletes the rule from the Business Rules page, or a token calls the delete endpoint. The row and its versions are gone; the payload carries what a mirror needs to drop its own copy. ### Related events - `business_rule.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `business_rule.deleted` | yes | The event name, always `business_rule.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.business_rule_id` | string (uuid) | yes | The rule's id. | | `data.name` | string | yes | What the rule was called. | | `data.project_id` | string (uuid) | yes | The project the rule belonged to. | | `data.reference` | string | yes | The rule's key, never reused. | | `data.type` | `constraint`, `derivation`, `computation`, `inference`, `action_enabler`, `policy` | yes | The kind. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VF4", "type": "business_rule.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "business_rule_id": "d5e6f7a8-b9c0-4d1e-8f2a-3b4c5d6e7f80", "name": "Platinum tier threshold", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-BR-7", "type": "inference" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # business_rule.quality_reviewed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-quality-reviewed A rule's quality is reviewed. ### When this fires A person presses Review now on the rule's flyout, an agent calls `review_quality`, or a token posts to the rule's quality reviews endpoint. The payload carries the score, how many findings the review holds and the version of the rule that was read; read the [business rules endpoint](/api/v1/reference/business-rules) for the findings themselves. ### Related events - `business_rule.updated`: the rule changes once the findings are addressed. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `business_rule.quality_reviewed` | yes | The event name, always `business_rule.quality_reviewed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.business_rule_id` | string (uuid) | yes | The rule's id. | | `data.findings_count` | integer | yes | How many findings the review holds; 0 when the record was found sound. | | `data.project_id` | string (uuid) | yes | The project the business rule belongs to. | | `data.reference` | string | yes | The business rule's reference. | | `data.review_id` | string (uuid) | yes | The review's id; read the endpoint for its findings. | | `data.score` | integer | yes | The score, 0 to 100. | | `data.version` | integer \| null | yes | The version of the business rule that was read; null when it had no version yet. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ1", "type": "business_rule.quality_reviewed", "created_at": "2026-10-28T11:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "business_rule_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "findings_count": 2, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-BR-7", "review_id": "b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d01", "score": 71, "version": 3 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # business_rule.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-restored An earlier version of a rule is put back. ### When this fires A person restores a version from the rule's history, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, the examples included, and the payload carries both numbers. ### Related events - `business_rule.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `business_rule.restored` | yes | The event name, always `business_rule.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.business_rule_id` | string (uuid) | yes | The rule's id. | | `data.name` | string | yes | What the rule is called after the restore. | | `data.project_id` | string (uuid) | yes | The project the rule belongs to. | | `data.reference` | string | yes | The rule's key. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VF3", "type": "business_rule.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "business_rule_id": "d5e6f7a8-b9c0-4d1e-8f2a-3b4c5d6e7f80", "name": "Platinum tier threshold", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-BR-7", "restored_from_version": 2, "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # business_rule.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-status-changed A rule moves along its review lifecycle. ### When this fires A person moves the rule from the Business Rules page or the flyout, or a token calls the status endpoint. The payload carries the status before and after, so a mirror that shows the client only agreed rules reacts without diffing a column. ### Related events - `business_rule.deleted`: the end of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `business_rule.status_changed` | yes | The event name, always `business_rule.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.business_rule_id` | string (uuid) | yes | The rule's id. | | `data.name` | string | yes | What the rule is called. | | `data.previous_status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | yes | Where the rule was. | | `data.project_id` | string (uuid) | yes | The project the rule belongs to. | | `data.reference` | string | yes | The rule's key. | | `data.status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | yes | Where the rule is now. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VF2", "type": "business_rule.status_changed", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "business_rule_id": "d5e6f7a8-b9c0-4d1e-8f2a-3b4c5d6e7f80", "name": "Platinum tier threshold", "previous_status": "in_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-BR-7", "status": "approved" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # business_rule.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-updated A rule's fields change. ### When this fires Any of the rule's own columns change: the name, the kind, the statement, the condition, the action, the exceptions, the examples, the authority, the priority, the dates in force or the owner. A status move raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after. ### Related events - `business_rule.status_changed`: the move that is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `business_rule.updated` | yes | The event name, always `business_rule.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.business_rule_id` | string (uuid) | yes | The rule's id. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.name` | string | yes | What the rule is called after the change. | | `data.project_id` | string (uuid) | yes | The project the rule belongs to. | | `data.reference` | string | yes | The rule's key. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VF1", "type": "business_rule.updated", "created_at": "2026-10-28T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "business_rule_id": "d5e6f7a8-b9c0-4d1e-8f2a-3b4c5d6e7f80", "changes": { "priority": { "from": "should", "to": "must" }, "effective_from": { "from": null, "to": "2027-01-01" } }, "name": "Platinum tier threshold", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-BR-7" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # business_rule.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule The lifecycle of a project's business rules: a rule added, changed, moved along its review lifecycle, put back to an earlier version and deleted. A mirror of the specification, a rules engine that reads the agreed rules and the people's notifications subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the reference and what the move itself carries, never a snapshot of the rule. Read the [business rules endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/business-rules) for the record and its versions. ## Required ability Tokens subscribing to `business_rule.*` events need `business-rule:view` at mint time. ## Events - [`business_rule.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-created) — A rule is added. - [`business_rule.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-updated) — A rule's fields change. - [`business_rule.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-status-changed) — A rule moves along its review lifecycle. - [`business_rule.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-restored) — An earlier version of a rule is put back. - [`business_rule.quality_reviewed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-quality-reviewed) — A rule's quality is reviewed. - [`business_rule.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-deleted) — A rule is deleted. --- # change_request.analysed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-analysed Heimdall read a change request against its baseline. ### When this fires A person presses the analysis on the Change Requests page, an agent calls `analyse_change_impact`, or a token calls the analyse endpoint. The draft is written on the request, the impact fields the analyst left empty are filled, and the payload carries the level the request now shows and how many records Heimdall named. ### Related events - `change_request.updated`: the analyst's own edits to the impact. - `council.convened`: the council sitting on the request's change-impact moment. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `change_request.analysed` | yes | The event name, always `change_request.analysed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.affected_count` | integer | yes | How many records Heimdall named as touched. | | `data.change_request_id` | string (uuid) | yes | The request's id. | | `data.impact_level` | `low`, `medium`, `high`, null | yes | The level the request shows after the reading; null when unset. | | `data.project_id` | string (uuid) | yes | The project the request belongs to. | | `data.reference` | string | yes | The request's key. | | `data.title` | string | yes | What the change is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VR3", "type": "change_request.analysed", "created_at": "2026-11-20T09:15:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "affected_count": 3, "change_request_id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d14", "impact_level": "medium", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-CR-2", "title": "Accept vouchers on subscriptions too" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # change_request.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-created A change request is raised. ### When this fires A person raises a request on the Change Requests page, a client guest raises one from their own view, Heimdall or an agent calls `raise_change_request`, or a token posts to the change requests endpoint. The payload carries the reference the request was numbered with, why it was raised and the baseline it changes. ### Related events - `change_request.analysed`: Heimdall's reading. - `change_request.status_changed`: the request's lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `change_request.created` | yes | The event name, always `change_request.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.baseline_id` | string \| null (uuid) | yes | The baseline the change is raised against; null when none. | | `data.change_request_id` | string (uuid) | yes | The request's id. | | `data.project_id` | string (uuid) | yes | The project the request belongs to. | | `data.reason` | `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt`, `scope_reduction` | yes | Why it was raised. | | `data.reference` | string | yes | The key it was numbered with, such as `CRM-CR-2`. | | `data.status` | `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified`, `closed` | yes | Where it starts, which is always `submitted`. | | `data.title` | string | yes | What the change is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VR0", "type": "change_request.created", "created_at": "2026-11-19T14:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "baseline_id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70", "change_request_id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d14", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reason": "client_request", "reference": "CRM-CR-2", "status": "submitted", "title": "Accept vouchers on subscriptions too" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # change_request.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-deleted A change request that was never agreed is deleted. ### When this fires A person deletes the request from the Change Requests page, Heimdall or an agent calls `delete_change_request`, or a token calls the delete endpoint. The row, its links and its versions are gone; the payload carries what a mirror needs to drop its own copy by reference. ### Related events - `change_request.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `change_request.deleted` | yes | The event name, always `change_request.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.change_request_id` | string (uuid) | yes | The request's id. | | `data.project_id` | string (uuid) | yes | The project the request belonged to. | | `data.reason` | `new_requirement`, `clarification`, `defect`, `regulatory`, `client_request`, `technical_debt`, `scope_reduction` | yes | Why it was raised. | | `data.reference` | string | yes | The request's key, never reused. | | `data.title` | string | yes | What the change was called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VR5", "type": "change_request.deleted", "created_at": "2026-11-23T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "change_request_id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d14", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reason": "client_request", "reference": "CRM-CR-2", "title": "Accept vouchers on subscriptions too" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # change_request.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-restored An earlier version of a change request is put back. ### When this fires A person restores a version from the request's history, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, and the payload carries both numbers. ### Related events - `change_request.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `change_request.restored` | yes | The event name, always `change_request.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.change_request_id` | string (uuid) | yes | The request's id. | | `data.project_id` | string (uuid) | yes | The project the request belongs to. | | `data.reference` | string | yes | The request's key. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.title` | string | yes | What the change is called after the restore. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VR4", "type": "change_request.restored", "created_at": "2026-11-22T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "change_request_id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d14", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-CR-2", "restored_from_version": 2, "title": "Accept vouchers on subscriptions too", "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # change_request.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-status-changed A change request moves between its states. ### When this fires A person moves the request by hand (into analysis, into the work, verified, closed), or the approval engine moves it into awaiting approval, approved or rejected. The payload carries the status before and after. A move into `approved` is the moment the baseline items the request touches are unlocked. ### Related events - `approval.approved` and `approval.rejected`: the verdicts behind the gate's moves. - `baseline.frozen`: the baseline the request changes. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `change_request.status_changed` | yes | The event name, always `change_request.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.change_request_id` | string (uuid) | yes | The request's id. | | `data.previous_status` | `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified`, `closed` | yes | Where the request was. | | `data.project_id` | string (uuid) | yes | The project the request belongs to. | | `data.reference` | string | yes | The request's key. | | `data.status` | `submitted`, `under_analysis`, `awaiting_approval`, `approved`, `rejected`, `implementing`, `implemented`, `verified`, `closed` | yes | Where the request is now. | | `data.title` | string | yes | What the change is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VR2", "type": "change_request.status_changed", "created_at": "2026-11-21T11:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "change_request_id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d14", "previous_status": "awaiting_approval", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-CR-2", "status": "approved", "title": "Accept vouchers on subscriptions too" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # change_request.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-updated A change request's fields change. ### When this fires Any of the request's own columns change: the title, the description, the reason, the stakeholder, the baseline, the impact fields, the priority; or the records it touches are set. A status move raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after; when the records touched changed, `changes.affected` lists them as `type:id` before and after. ### Related events - `change_request.status_changed`: the move that is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `change_request.updated` | yes | The event name, always `change_request.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.change_request_id` | string (uuid) | yes | The request's id. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.project_id` | string (uuid) | yes | The project the request belongs to. | | `data.reference` | string | yes | The request's key. | | `data.title` | string | yes | What the change is called after the update. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VR1", "type": "change_request.updated", "created_at": "2026-11-20T09:40:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "change_request_id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d14", "changes": { "impact_level": { "from": null, "to": "medium" }, "impact_schedule_days": { "from": null, "to": 5 } }, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-CR-2", "title": "Accept vouchers on subscriptions too" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # change_request.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request The life of a project's change requests: a request raised, its words or impact changed, moved along its lifecycle by hand or by the approval engine, read by Heimdall for its impact, put back to an earlier version and deleted. A client portal that shows what was asked for and what it costs, a planning tool that moves dates when a change is approved, and an audit trail of what changed after the scope was signed subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the reference, the reason and what the move itself carries, never the impact in full. Read the [change requests endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/change-requests) for the record with its impact and versions, and the [`approval.*` family](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval) for the request behind an approval or a rejection. ## Required ability Tokens subscribing to `change_request.*` events need `change-request:view` at mint time. ## Events - [`change_request.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-created) — A change request is raised. - [`change_request.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-updated) — A change request's fields change. - [`change_request.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-status-changed) — A change request moves between its states. - [`change_request.analysed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-analysed) — Heimdall read a change request against its baseline. - [`change_request.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-restored) — An earlier version of a change request is put back. - [`change_request.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-deleted) — A change request that was never agreed is deleted. --- # client.archived Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-archived A client is archived. ### When this fires Someone archives the client. It leaves the Clients page's default view and the default listing, while its projects and its history stay readable; restoring brings it back as it was. ### Related events - `client.restored`: the way back. - `client.deleted`: the terminal state, open only to a client with no project. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `client.archived` | yes | The event name, always `client.archived` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.client_id` | string (uuid) | yes | The client. | | `data.name` | string | yes | The client's name. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V72", "type": "client.archived", "created_at": "2027-03-01T16:40:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "client_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Acme Retail" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # client.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-created A client is added. ### When this fires A person adds a client on the Clients page, or a token creates one through the API. The payload carries the id a mirror needs to open the client and the status the relationship starts in. ### Related events - `client.updated`: the next change. - `project.created`: the first project done for the client. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `client.created` | yes | The event name, always `client.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.client_id` | string (uuid) | yes | The client's id. | | `data.name` | string | yes | The client's name, unique within the team. | | `data.status` | `prospect`, `active`, `dormant`, `churned` | yes | Where the relationship starts, `active` unless the person chose otherwise. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V70", "type": "client.created", "created_at": "2026-11-02T09:40:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "client_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Acme Retail", "status": "active" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # client.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-deleted A client that no project names is deleted. ### When this fires Someone deletes a client, which the application allows only while no project, archived or deleted ones included, names it; otherwise the delete is refused and the client is archived instead. There is no restore. ### Related events - `client.archived`: the reversible alternative. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `client.deleted` | yes | The event name, always `client.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.client_id` | string (uuid) | yes | The client that no longer exists. | | `data.name` | string | yes | The client's last name. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V74", "type": "client.deleted", "created_at": "2027-06-30T12:00:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "client_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Acme Retail" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # client.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-restored An archived client is restored. ### When this fires Someone restores an archived client. It returns to the default view with everything it had. ### Related events - `client.archived`: what it undoes. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `client.restored` | yes | The event name, always `client.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.client_id` | string (uuid) | yes | The client. | | `data.name` | string | yes | The client's name. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V73", "type": "client.restored", "created_at": "2027-03-04T08:10:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "client_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Acme Retail" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # client.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-updated A client's fields change. ### When this fires Any of the client's own columns change: its names, website, industry, country, timezone, primary contact, notes or status. Archiving raises its own event, not this one. ### Related events - `client.archived`: archiving, which is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `client.updated` | yes | The event name, always `client.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each mapped to an object with `from` and `to`. Only the changed columns appear. | | `data.client_id` | string (uuid) | yes | The client. | | `data.name` | string | yes | The client's name after the change. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V71", "type": "client.updated", "created_at": "2026-11-20T11:15:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "changes": { "status": { "from": "active", "to": "dormant" }, "primary_contact_email": { "from": "jane@acme.example", "to": "ops@acme.example" } }, "client_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Acme Retail" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # client.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client The lifecycle of a client: creation, edits, archiving, restoring and deletion. A client is the organisation a project is done for, so a mirror of the registry subscribes to this family beside `project.*` to keep the two in step. Every event in the family is scoped to the team and carries no `project_id` in the envelope: a client belongs to the team, not to one of its projects, so an endpoint confined to one project never receives these. The payloads are thin on purpose: the id, the name and what the transition itself carries, never a snapshot of the client. Read the [clients endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/clients) for the record. ## Required ability Tokens subscribing to `client.*` events need `client:view` at mint time. ## Events - [`client.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-created) — A client is added. - [`client.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-updated) — A client's fields change. - [`client.archived`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-archived) — A client is archived. - [`client.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-restored) — An archived client is restored. - [`client.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-deleted) — A client that no project names is deleted. --- # comment.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-created A comment is posted. ### When this fires A person posts a comment from an artefact's flyout, an agent posts one with `comment_on_item`, or a token creates one through the API. The payload carries the artefact, the author, whether the comment is internal and, for a reply, the first comment of the thread. ### Related events - `comment.updated`: the words change. - `comment.deleted`: the end of the comment. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `comment.created` | yes | The event name, always `comment.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.artefact_id` | string (uuid) | yes | The artefact the comment is on. | | `data.artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The artefact's type. | | `data.author_id` | string \| null (uuid) | yes | The person who wrote it; null when the account is gone. | | `data.comment_id` | string (uuid) | yes | The comment's id. | | `data.internal` | boolean | yes | Whether the comment is internal, never shown to a client guest. | | `data.parent_id` | string \| null (uuid) | yes | The first comment of the thread answered; null for a first comment. | | `data.project_id` | string (uuid) | yes | The project the artefact belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK2", "type": "comment.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "artefact_type": "work-item", "author_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "comment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "internal": false, "parent_id": null, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # comment.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-deleted A comment is deleted. ### When this fires The author or a moderator deletes a comment from the flyout or through the API. The replies under a first comment go with it, and one event fires for the comment named, so a mirror drops the thread by its first comment. ### Related events - `comment.created`: the comment's beginning. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `comment.deleted` | yes | The event name, always `comment.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.artefact_id` | string (uuid) | yes | The artefact the comment was on. | | `data.artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The artefact's type. | | `data.author_id` | string \| null (uuid) | yes | The person who wrote it; null when the account is gone. | | `data.comment_id` | string (uuid) | yes | The comment's id. | | `data.internal` | boolean | yes | Whether the comment was internal. | | `data.parent_id` | string \| null (uuid) | yes | The first comment of the thread it answered; null for a first comment, whose replies went with it. | | `data.project_id` | string (uuid) | yes | The project the artefact belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK6", "type": "comment.deleted", "created_at": "2026-11-02T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "artefact_type": "work-item", "author_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "comment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "internal": false, "parent_id": null, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # comment.reopened Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-reopened A resolved thread is marked open again. ### When this fires The author or a moderator reopens a thread from the flyout or through the reopen endpoint. An open thread reopened fires nothing. ### Related events - `comment.resolved`: the thread is closed again. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `comment.reopened` | yes | The event name, always `comment.reopened` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.artefact_id` | string (uuid) | yes | The artefact the thread is on. | | `data.artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The artefact's type. | | `data.author_id` | string \| null (uuid) | yes | The person who wrote the first comment; null when the account is gone. | | `data.comment_id` | string (uuid) | yes | The first comment of the thread. | | `data.internal` | boolean | yes | Whether the thread is internal. | | `data.parent_id` | string \| null (uuid) | yes | Always null: only a first comment is reopened. | | `data.project_id` | string (uuid) | yes | The project the artefact belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK5", "type": "comment.reopened", "created_at": "2026-10-29T09:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "artefact_type": "work-item", "author_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "comment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "internal": false, "parent_id": null, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # comment.resolved Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-resolved The first comment of a thread is marked resolved. ### When this fires The author or a moderator resolves a thread from the flyout or through the resolve endpoint. A resolved thread resolved again fires nothing. ### Related events - `comment.reopened`: the thread opens again. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `comment.resolved` | yes | The event name, always `comment.resolved` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.artefact_id` | string (uuid) | yes | The artefact the thread is on. | | `data.artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The artefact's type. | | `data.author_id` | string \| null (uuid) | yes | The person who wrote the first comment; null when the account is gone. | | `data.comment_id` | string (uuid) | yes | The first comment of the thread. | | `data.internal` | boolean | yes | Whether the thread is internal. | | `data.parent_id` | string \| null (uuid) | yes | Always null: only a first comment is resolved. | | `data.project_id` | string (uuid) | yes | The project the artefact belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK4", "type": "comment.resolved", "created_at": "2026-10-29T08:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "artefact_type": "work-item", "author_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "comment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "internal": false, "parent_id": null, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # comment.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-updated A comment's words are rewritten. ### When this fires The author, or a moderator with `comment:update`, rewrites a comment from the flyout or through the API. The payload is the same shape as `comment.created`; read the endpoint for the new words. ### Related events - `comment.created`: the comment's beginning. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `comment.updated` | yes | The event name, always `comment.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.artefact_id` | string (uuid) | yes | The artefact the comment is on. | | `data.artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The artefact's type. | | `data.author_id` | string \| null (uuid) | yes | The person who wrote it; null when the account is gone. | | `data.comment_id` | string (uuid) | yes | The comment's id. | | `data.internal` | boolean | yes | Whether the comment is internal. | | `data.parent_id` | string \| null (uuid) | yes | The first comment of the thread answered; null for a first comment. | | `data.project_id` | string (uuid) | yes | The project the artefact belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK3", "type": "comment.updated", "created_at": "2026-10-28T10:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "artefact_type": "work-item", "author_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "comment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "internal": false, "parent_id": null, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # comment.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment The conversations on a project's artefacts: a comment posted, edited, resolved, reopened and deleted. A chat bridge, a wiki that mirrors the discussion and a bot that answers questions subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads carry the artefact by type and id, the author, the audience and the thread, never the words: read the [comments endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/comments) for the body. An internal comment is announced like any other, because endpoints are the team's; keep that in mind before you mirror the family somewhere a client reads. ## Required ability Tokens subscribing to `comment.*` events need `comment:view` at mint time. ## Events - [`comment.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-created) — A comment is posted. - [`comment.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-updated) — A comment's words are rewritten. - [`comment.resolved`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-resolved) — The first comment of a thread is marked resolved. - [`comment.reopened`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-reopened) — A resolved thread is marked open again. - [`comment.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-deleted) — A comment is deleted. --- # component.archived Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-archived A component is archived. ### When this fires A person archives the component from the Components page, or a token calls the archive endpoint. Its environments and its history are kept; it leaves the default view. ### Related events - `component.restored`: the way back. - `component.deleted`: the terminal state, open only to a component no environment runs. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `component.archived` | yes | The event name, always `component.archived` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.component_id` | string (uuid) | yes | The component's id. | | `data.name` | string | yes | The component's name. | | `data.project_id` | string (uuid) | yes | The project the component belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V82", "type": "component.archived", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "component_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "name": "Legacy Portal", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # component.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-created A component is added. ### When this fires A person adds a component on the Components page or in the intake wizard, or a token creates one through the API. The payload carries the id a mirror needs to open the component, the slug a version string will name it by and the kind that says what it is. ### Related events - `component.updated`: the next change. - `environment.created`: the first place the component runs. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `component.created` | yes | The event name, always `component.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.component_id` | string (uuid) | yes | The component's id. | | `data.kind` | `web_app`, `api`, `mobile_ios`, `mobile_android`, `mobile_cross_platform`, `desktop`, `service`, `integration`, `database`, `infrastructure`, `other` | yes | What the component is. | | `data.name` | string | yes | The component's name, unique within the project. | | `data.project_id` | string (uuid) | yes | The project the component belongs to. | | `data.slug` | string | yes | The slug a version string names the component by. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V80", "type": "component.created", "created_at": "2026-10-20T09:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "component_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "kind": "web_app", "name": "Customer Web App", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "slug": "web" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # component.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-deleted A component no environment runs is deleted. ### When this fires A person confirms the delete on the Components page, or a token calls the delete endpoint, and no environment runs the component. The payload carries the last facts about a row that no longer exists. ### Related events - `component.archived`: the reversible alternative. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `component.deleted` | yes | The event name, always `component.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.component_id` | string (uuid) | yes | The id the component had. | | `data.name` | string | yes | The name the component had. | | `data.project_id` | string (uuid) | yes | The project the component belonged to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V84", "type": "component.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "component_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "name": "Legacy Portal", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # component.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-restored An archived component is restored. ### When this fires A person restores the component from the Components page with the archived switch on, or a token calls the restore endpoint. ### Related events - `component.archived`: what it undoes. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `component.restored` | yes | The event name, always `component.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.component_id` | string (uuid) | yes | The component's id. | | `data.name` | string | yes | The component's name. | | `data.project_id` | string (uuid) | yes | The project the component belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V83", "type": "component.restored", "created_at": "2026-11-06T09:10:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "component_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "name": "Legacy Portal", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # component.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-updated A component's fields change. ### When this fires Any of the component's own columns change: its name, slug, kind, description, stack tags or owner. Archiving raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after; the stack arrives as a list. ### Related events - `component.archived`: archiving, which is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `component.updated` | yes | The event name, always `component.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`; the stack as a list of tags. | | `data.component_id` | string (uuid) | yes | The component's id. | | `data.name` | string | yes | The component's name after the change. | | `data.project_id` | string (uuid) | yes | The project the component belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V81", "type": "component.updated", "created_at": "2026-10-27T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "owner_id": { "from": null, "to": "f0e1d2c3-b4a5-4968-8776-655443322110" }, "tech_stack": { "from": [ "Laravel 13" ], "to": [ "Laravel 13", "Livewire 4" ] } }, "component_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "name": "Customer Web App", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # component.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component The lifecycle of a project's components: creation, edits, archiving, restoring and deletion. A component is one application the project is made of, so a mirror of the project record subscribes to this family beside `project.*` and `environment.*` to know what is being built and where it runs. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the name and what the transition itself carries, never a snapshot of the component. Read the [components endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/components) for the record. ## Required ability Tokens subscribing to `component.*` events need `component:view` at mint time. ## Events - [`component.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-created) — A component is added. - [`component.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-updated) — A component's fields change. - [`component.archived`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-archived) — A component is archived. - [`component.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-restored) — An archived component is restored. - [`component.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-deleted) — A component no environment runs is deleted. --- # council.convened Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-convened council.convened ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `council.convened` | yes | The event name, always `council.convened` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # council.failed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-failed council.failed ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `council.failed` | yes | The event name, always `council.failed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # council.finding_accepted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-accepted council.finding_accepted ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `council.finding_accepted` | yes | The event name, always `council.finding_accepted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # council.finding_dismissed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-dismissed council.finding_dismissed ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `council.finding_dismissed` | yes | The event name, always `council.finding_dismissed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # council.ready Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-ready council.ready ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `council.ready` | yes | The event name, always `council.ready` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # council.round_opened Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-round-opened council.round_opened ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `council.round_opened` | yes | The event name, always `council.round_opened` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # council.voice_spoke Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-voice-spoke council.voice_spoke ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `council.voice_spoke` | yes | The event name, always `council.voice_spoke` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # council.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council ## Events - [`council.convened`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-convened) — council.convened - [`council.voice_spoke`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-voice-spoke) — council.voice_spoke - [`council.round_opened`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-round-opened) — council.round_opened - [`council.ready`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-ready) — council.ready - [`council.failed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-failed) — council.failed - [`council.finding_accepted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-accepted) — council.finding_accepted - [`council.finding_dismissed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-dismissed) — council.finding_dismissed --- # data_entity.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-created An entity is created. ### When this fires A person creates an entity on the Data Entities page or in the intake wizard, or a token creates one through the API. The entity has no fields yet; they arrive through a fields replacement, which raises `data_entity.fields_changed`. ### Related events - `data_entity.fields_changed`: the fields arriving. - `data_entity.updated`: the next change. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `data_entity.created` | yes | The event name, always `data_entity.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.data_entity_id` | string (uuid) | yes | The entity's id, the value a reference field carries. | | `data.name` | string | yes | The entity's name, unique within the project. | | `data.project_id` | string (uuid) | yes | The project the entity belongs to. | | `data.status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the entity starts: `draft` unless the creator said otherwise. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC0", "type": "data_entity.created", "created_at": "2026-10-20T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "data_entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "name": "Order", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "status": "draft" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # data_entity.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-deleted An entity is deleted. ### When this fires A person deletes the entity from the Data Entities page, or a token calls the delete endpoint. The row and its fields are gone, and a reference field of another entity that pointed at it has lost its target; the payload carries what a mirror needs to drop its own. ### Related events - `data_entity.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `data_entity.deleted` | yes | The event name, always `data_entity.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.data_entity_id` | string (uuid) | yes | The entity's id. | | `data.name` | string | yes | The entity's name. | | `data.project_id` | string (uuid) | yes | The project the entity belonged to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC4", "type": "data_entity.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "data_entity_id": "b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d0e", "name": "Invoice", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # data_entity.fields_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-fields-changed The entity's fields are replaced. ### When this fires A person saves the entity's fields on the Data Entities page, or a token calls the fields endpoint, and at least one field was added, changed or removed; a replacement that changes nothing raises nothing. The payload carries the counts and the number of fields the entity has now; read the entity for the fields themselves. ### Related events - `data_entity.created`: the entity the fields belong to. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `data_entity.fields_changed` | yes | The event name, always `data_entity.fields_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.added` | integer | yes | How many fields the replacement added. | | `data.changed` | integer | yes | How many kept fields it changed. | | `data.data_entity_id` | string (uuid) | yes | The entity's id. | | `data.field_count` | integer | yes | How many fields the entity carries now. | | `data.name` | string | yes | The entity's name. | | `data.project_id` | string (uuid) | yes | The project the entity belongs to. | | `data.removed` | integer | yes | How many fields the replacement removed. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC3", "type": "data_entity.fields_changed", "created_at": "2026-11-06T09:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "added": 1, "changed": 1, "data_entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "field_count": 3, "name": "Order", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "removed": 0 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # data_entity.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-restored An earlier version of a entity is put back. ### When this fires A person restores a version from the entity's history on the Data Entities tab, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, and the payload carries both numbers. ### Related events - `data_entity.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `data_entity.restored` | yes | The event name, always `data_entity.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.data_entity_id` | string (uuid) | yes | The entity's id. | | `data.name` | string | yes | The entity's name after the restore. | | `data.project_id` | string (uuid) | yes | The project the entity belongs to. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VKC", "type": "data_entity.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "data_entity_id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "name": "Customer", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "restored_from_version": 2, "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # data_entity.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-status-changed An entity moves along its review. ### When this fires A person moves the entity from the Data Entities page, or a token calls the status endpoint. The payload carries the status before and after, so a listener that mirrors the approved dictionary adds the entity when `status` becomes `approved` and drops it when `previous_status` was. ### Related events - `data_entity.updated`: a change to the entity itself. - `data_entity.deleted`: the terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `data_entity.status_changed` | yes | The event name, always `data_entity.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.data_entity_id` | string (uuid) | yes | The entity's id. | | `data.name` | string | yes | The entity's name. | | `data.previous_status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the entity was. | | `data.project_id` | string (uuid) | yes | The project the entity belongs to. | | `data.status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the entity is now. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC2", "type": "data_entity.status_changed", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "data_entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "name": "Order", "previous_status": "in_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "status": "approved" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # data_entity.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-updated An entity's fields change. ### When this fires Any of the entity's own columns change: the name, the plural, the description, the component, the owner, the personal-data flag or the retention note. A status move and a fields replacement raise their own events, not this one. `changes` holds one entry per column that changed, each with the value before and after. ### Related events - `data_entity.fields_changed`: a change to the fields, which is deliberately not an update. - `data_entity.status_changed`: a move along the review. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `data_entity.updated` | yes | The event name, always `data_entity.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.data_entity_id` | string (uuid) | yes | The entity's id. | | `data.name` | string | yes | The entity's name after the change. | | `data.project_id` | string (uuid) | yes | The project the entity belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC1", "type": "data_entity.updated", "created_at": "2026-10-27T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "owner_id": { "from": null, "to": "9b2e4f1a-3c5d-4e6f-8a7b-1c2d3e4f5a6b" }, "pii": { "from": false, "to": true } }, "data_entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "name": "Order", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # data_entity.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity The lifecycle of a project's data dictionary: an entity created, changed, moved along its review, its fields replaced and the entity deleted. Requirements, business rules, API contracts and database designs are read against the dictionary, so a mirror of the planning record, a schema generator or a data-protection register subscribes to this family to follow it. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the name and what the transition itself carries, never a snapshot of the entity or its fields. Read the [data entities endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/data-entities) for the record. ## Required ability Tokens subscribing to `data_entity.*` events need `data-entity:view` at mint time. ## Events - [`data_entity.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-created) — An entity is created. - [`data_entity.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-updated) — An entity's fields change. - [`data_entity.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-status-changed) — An entity moves along its review. - [`data_entity.fields_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-fields-changed) — The entity's fields are replaced. - [`data_entity.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-deleted) — An entity is deleted. - [`data_entity.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-restored) — An earlier version of a entity is put back. --- # decision.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-created A decision is added. ### When this fires A person adds a decision on the Decisions page, an agent records a draft with `record_decision_draft`, or a token creates one through the API. The payload carries the reference the decision was numbered with, its kind and the status it starts in. ### Related events - `decision.updated`: the next change. - `decision.status_changed`: the decision's lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `decision.created` | yes | The event name, always `decision.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.decision_id` | string (uuid) | yes | The decision's id. | | `data.kind` | `architecture`, `product`, `process`, `vendor`, `security` | yes | The kind. | | `data.project_id` | string (uuid) | yes | The project the decision belongs to. | | `data.reference` | string | yes | The key it was numbered with, such as `CRM-DEC-5`. | | `data.status` | `proposed`, `accepted`, `rejected`, `superseded`, `deprecated` | yes | Where it starts: `proposed` unless the creator said otherwise. | | `data.title` | string | yes | What the decision is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VJ0", "type": "decision.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "decision_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "kind": "architecture", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-DEC-5", "status": "proposed", "title": "Keep the loyalty ledger in Postgres" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # decision.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-deleted A decision is deleted. ### When this fires A person deletes the decision from the Decisions page, or a token calls the delete endpoint. The row and its versions are gone; the payload carries what a mirror needs to drop its own copy. ### Related events - `decision.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `decision.deleted` | yes | The event name, always `decision.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.decision_id` | string (uuid) | yes | The decision's id. | | `data.kind` | `architecture`, `product`, `process`, `vendor`, `security` | yes | The kind it was. | | `data.project_id` | string (uuid) | yes | The project the decision belonged to. | | `data.reference` | string | yes | The decision's key, never reused. | | `data.title` | string | yes | What the decision was called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VJ4", "type": "decision.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "decision_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "kind": "architecture", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-DEC-5", "title": "Keep the loyalty ledger in Postgres" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # decision.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-restored An earlier version of a decision is put back. ### When this fires A person restores a version from the decision's history, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, and the payload carries both numbers. ### Related events - `decision.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `decision.restored` | yes | The event name, always `decision.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.decision_id` | string (uuid) | yes | The decision's id. | | `data.project_id` | string (uuid) | yes | The project the decision belongs to. | | `data.reference` | string | yes | The decision's key. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.title` | string | yes | What the decision is called after the restore. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VJ3", "type": "decision.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "decision_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-DEC-5", "restored_from_version": 2, "title": "Keep the loyalty ledger in Postgres", "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # decision.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-status-changed A decision moves along its lifecycle. ### When this fires A person moves the decision from the Decisions page or the flyout, or a token calls the status endpoint. The payload carries the status before and after. Accepting a decision that names an earlier one moves that one to superseded and raises this event for it too. ### Related events - `decision.deleted`: the end of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `decision.status_changed` | yes | The event name, always `decision.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.decision_id` | string (uuid) | yes | The decision's id. | | `data.previous_status` | `proposed`, `accepted`, `rejected`, `superseded`, `deprecated` | yes | Where the decision was. | | `data.project_id` | string (uuid) | yes | The project the decision belongs to. | | `data.reference` | string | yes | The decision's key. | | `data.status` | `proposed`, `accepted`, `rejected`, `superseded`, `deprecated` | yes | Where the decision is now. | | `data.title` | string | yes | What the decision is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VJ2", "type": "decision.status_changed", "created_at": "2026-11-04T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "decision_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "previous_status": "proposed", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-DEC-5", "status": "accepted", "title": "Keep the loyalty ledger in Postgres" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # decision.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-updated A decision's fields change. ### When this fires Any of the decision's own columns change: the kind, the title, the context, the options, what was decided, the consequences, the deciders or the decision it supersedes. A status move raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after. ### Related events - `decision.status_changed`: the move that is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `decision.updated` | yes | The event name, always `decision.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.decision_id` | string (uuid) | yes | The decision's id. | | `data.project_id` | string (uuid) | yes | The project the decision belongs to. | | `data.reference` | string | yes | The decision's key. | | `data.title` | string | yes | What the decision is called after the change. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VJ1", "type": "decision.updated", "created_at": "2026-10-29T10:15:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "consequences": { "from": null, "to": "<p>The reporting queries join the ledger directly.</p>" } }, "decision_id": "a8b9c0d1-e2f3-4a4b-9c5d-6e7f8a9b0c13", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-DEC-5", "title": "Keep the loyalty ledger in Postgres" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # decision.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision The lifecycle of a project's decisions: a decision added, changed, moved along its lifecycle, put back to an earlier version and deleted. A wiki that mirrors the decision records, a context pack builder and the people's notifications subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the reference, the kind and what the move itself carries, never a snapshot of the decision. Read the [decisions endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/decisions) for the record and its versions. ## Required ability Tokens subscribing to `decision.*` events need `decision:view` at mint time. ## Events - [`decision.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-created) — A decision is added. - [`decision.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-updated) — A decision's fields change. - [`decision.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-status-changed) — A decision moves along its lifecycle. - [`decision.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-restored) — An earlier version of a decision is put back. - [`decision.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-deleted) — A decision is deleted. --- # design_reference.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-created A reference is added. ### When this fires A person adds a reference on the Designs page or in the intake wizard, or a token creates one through the API. The payload carries the kind, so a listener that cares about contracts alone can act on `api_contract` and ignore the rest. ### Related events - `design_reference.updated`: the next change. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `design_reference.created` | yes | The event name, always `design_reference.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.design_reference_id` | string (uuid) | yes | The reference's id. | | `data.kind` | `figma`, `wireframe`, `prototype`, `screenshot`, `api_contract`, `sequence_diagram`, `erd`, `document`, `other` | yes | What the link points at. | | `data.project_id` | string (uuid) | yes | The project the reference belongs to. | | `data.title` | string | yes | What the reference is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD0", "type": "design_reference.created", "created_at": "2026-10-20T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "design_reference_id": "e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a01", "kind": "figma", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "title": "Checkout flow" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # design_reference.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-deleted A reference is deleted. ### When this fires A person deletes the reference from the Designs page, or a token calls the delete endpoint. The row is gone; the design stays wherever the designer keeps it, and the payload carries what a mirror needs to drop its own row. ### Related events - `design_reference.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `design_reference.deleted` | yes | The event name, always `design_reference.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.design_reference_id` | string (uuid) | yes | The reference's id. | | `data.project_id` | string (uuid) | yes | The project the reference belonged to. | | `data.title` | string | yes | What the reference was called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD2", "type": "design_reference.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "design_reference_id": "e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "title": "Checkout flow" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # design_reference.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-updated A reference's fields change. ### When this fires Any of the reference's own columns change: the title, the address, the kind, what it is attached to, the version or the notes. `changes` holds one entry per column that changed, each with the value before and after; a move between the project and a component shows as `referenceable_type` and `referenceable_id` changing together. ### Related events - `design_reference.deleted`: the terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `design_reference.updated` | yes | The event name, always `design_reference.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.design_reference_id` | string (uuid) | yes | The reference's id. | | `data.project_id` | string (uuid) | yes | The project the reference belongs to. | | `data.title` | string | yes | What the reference is called after the change. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD1", "type": "design_reference.updated", "created_at": "2026-10-27T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "version": { "from": "v3", "to": "v4" } }, "design_reference_id": "e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "title": "Checkout flow" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # design_reference.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference The lifecycle of a project's design references: a link added, changed and removed. The references are what developers build against and the client signs off against, so a mirror of the planning record, a design-review bot or a changelog subscribes to this family to follow which design is current. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the title and what the transition itself carries, never a snapshot of the reference. Read the [design references endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/design-references) for the record. ## Required ability Tokens subscribing to `design_reference.*` events need `design-reference:view` at mint time. ## Events - [`design_reference.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-created) — A reference is added. - [`design_reference.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-updated) — A reference's fields change. - [`design_reference.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-deleted) — A reference is deleted. --- # environment.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-created An environment is added. ### When this fires A person adds an environment on the Environments page or in the intake wizard, or a token creates one through the API. The payload carries the id a mirror needs to open the environment, the kind that says what it is for, the component it runs and the address people open. ### Related events - `environment.updated`: the next change. - `environment.health_changed`: the first check, once a health-check address is set. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `environment.created` | yes | The event name, always `environment.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.component_id` | string \| null (uuid) | yes | The component the environment runs; null when it serves the whole project. | | `data.environment_id` | string (uuid) | yes | The environment's id. | | `data.kind` | `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom` | yes | What the environment is for. | | `data.name` | string | yes | The environment's name, unique within the project and component. | | `data.project_id` | string (uuid) | yes | The project the environment belongs to. | | `data.url` | string \| null | yes | The address people open; null when none was given. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V90", "type": "environment.created", "created_at": "2026-10-20T09:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "component_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "environment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7081", "kind": "staging", "name": "Staging", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "url": "https://staging.example.test" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # environment.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-deleted An environment is deleted. ### When this fires A person confirms the delete on the Environments page, or a token calls the delete endpoint. The payload carries the last facts about a row that no longer exists. ### Related events - `environment.created`: the beginning of the record this ends. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `environment.deleted` | yes | The event name, always `environment.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.environment_id` | string (uuid) | yes | The id the environment had. | | `data.name` | string | yes | The name the environment had. | | `data.project_id` | string (uuid) | yes | The project the environment belonged to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V92", "type": "environment.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "environment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7081", "name": "Scratch", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # environment.frozen Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-frozen A change freeze starts. ### When this fires A release manager freezes the environment from the Environments page, or a pipeline token calls the freeze endpoint ahead of a release window. Until `frozen_until`, deployments into the environment are refused; a pipeline that listens here holds its deployments without asking. ### Related events - `environment.unfrozen`: the freeze lifted early; a freeze that runs out raises nothing, the end was announced here. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `environment.frozen` | yes | The event name, always `environment.frozen` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.environment_id` | string (uuid) | yes | The environment's id. | | `data.frozen_until` | string (date-time) | yes | When the freeze ends. | | `data.name` | string | yes | The environment's name. | | `data.project_id` | string (uuid) | yes | The project the environment belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V93", "type": "environment.frozen", "created_at": "2026-12-20T08:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "environment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7081", "frozen_until": "2026-12-31T23:59:00+00:00", "name": "Production", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # environment.health_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-health-changed A check finds the environment in a different health than before. ### When this fires The scheduled check calls the environment's health-check address every few minutes, and a monitoring system or a person can record what they observed over the API; whichever wrote the health, this fires only when it differs from the health before, so a healthy environment checked all day stays quiet. A status page or a chat alert subscribes here; the maintenance module will open an incident when a production environment stays down. ### Related events - `environment.updated`: a change to the check's address, which is an edit rather than a health change. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `environment.health_changed` | yes | The event name, always `environment.health_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.checked_at` | string (date-time) | yes | When the check ran, or the report was recorded. | | `data.environment_id` | string (uuid) | yes | The environment's id. | | `data.health` | `healthy`, `degraded`, `down`, `unknown` | yes | What the check found. | | `data.kind` | `local`, `development`, `qa`, `staging`, `uat`, `preproduction`, `production`, `custom` | yes | What the environment is for, so a listener can act on production alone. | | `data.name` | string | yes | The environment's name. | | `data.previous_health` | `healthy`, `degraded`, `down`, `unknown` | yes | The health before this check. | | `data.project_id` | string (uuid) | yes | The project the environment belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V95", "type": "environment.health_changed", "created_at": "2026-10-27T11:25:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "checked_at": "2026-10-27T11:25:00+00:00", "environment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7081", "health": "down", "kind": "production", "name": "Production", "previous_health": "healthy", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # environment.unfrozen Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-unfrozen A change freeze is lifted before it ends. ### When this fires A release manager lifts the freeze from the Environments page, or a token calls the unfreeze endpoint. Deployments may resume. ### Related events - `environment.frozen`: what it undoes. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `environment.unfrozen` | yes | The event name, always `environment.unfrozen` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.environment_id` | string (uuid) | yes | The environment's id. | | `data.name` | string | yes | The environment's name. | | `data.project_id` | string (uuid) | yes | The project the environment belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V94", "type": "environment.unfrozen", "created_at": "2026-12-28T14:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "environment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7081", "name": "Production", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # environment.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-updated An environment's fields change. ### When this fires Any of the environment's own columns change: its name, kind, component, addresses, branch, hosting, region, flags, check address, version or notes. The freeze and the health raise their own events, not this one. `changes` holds one entry per column that changed, each with the value before and after. ### Related events - `environment.frozen`: a freeze, which is deliberately not an update. - `environment.health_changed`: a health change, which is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `environment.updated` | yes | The event name, always `environment.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.environment_id` | string (uuid) | yes | The environment's id. | | `data.name` | string | yes | The environment's name after the change. | | `data.project_id` | string (uuid) | yes | The project the environment belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V91", "type": "environment.updated", "created_at": "2026-10-27T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "branch": { "from": "develop", "to": "release/1.4" }, "current_version": { "from": "1.3.2", "to": "1.4.0-rc.2" } }, "environment_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7081", "name": "Staging", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # environment.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment The lifecycle and the state of a project's environments: creation, edits, deletion, change freezes and the health the checks find. An environment is one place the project's software runs, so a deployment pipeline, a status page or a chat alert subscribes here to learn where it may deploy and what is up. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the name and what the transition itself carries, never a snapshot of the environment and never a secret. Read the [environments endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/environments) for the record. ## Required ability Tokens subscribing to `environment.*` events need `environment:view` at mint time. ## Events - [`environment.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-created) — An environment is added. - [`environment.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-updated) — An environment's fields change. - [`environment.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-deleted) — An environment is deleted. - [`environment.frozen`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-frozen) — A change freeze starts. - [`environment.unfrozen`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-unfrozen) — A change freeze is lifted before it ends. - [`environment.health_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-health-changed) — A check finds the environment in a different health than before. --- # epic.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-created An epic is added. ### When this fires A person adds an epic on the Epics page, a brief import is accepted, or a token creates one through the API. The payload carries the reference the epic was numbered with, the status it starts in and its priority. ### Related events - `epic.updated`: the next change. - `epic.status_changed`: the epic's lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `epic.created` | yes | The event name, always `epic.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.epic_id` | string (uuid) | yes | The epic's id. | | `data.priority` | `must`, `should`, `could`, `wont` | yes | Its MoSCoW priority. | | `data.project_id` | string (uuid) | yes | The project the epic belongs to. | | `data.reference` | string | yes | The key the epic was numbered with, such as `CRM-12`. | | `data.status` | `draft`, `in_review`, `approved`, `in_progress`, `done`, `cancelled` | yes | Where the epic starts: `draft` unless the creator said otherwise. | | `data.title` | string | yes | What the epic is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC0", "type": "epic.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "priority": "must", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-12", "status": "draft", "title": "Loyalty tiers" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # epic.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-deleted An epic is deleted. ### When this fires A person deletes the epic from the Epics page, or a token calls the delete endpoint. The row and its versions are gone; the payload carries what a mirror needs to drop its own copy. ### Related events - `epic.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `epic.deleted` | yes | The event name, always `epic.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.epic_id` | string (uuid) | yes | The epic's id. | | `data.project_id` | string (uuid) | yes | The project the epic belonged to. | | `data.reference` | string | yes | The epic's key, never reused. | | `data.title` | string | yes | What the epic was called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC4", "type": "epic.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-12", "title": "Loyalty tiers" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # epic.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-restored An earlier version of an epic is put back. ### When this fires A person restores a version from the epic's history, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, and the payload carries both numbers, so a mirror re-reads the record rather than guessing what moved. ### Related events - `epic.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `epic.restored` | yes | The event name, always `epic.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.epic_id` | string (uuid) | yes | The epic's id. | | `data.project_id` | string (uuid) | yes | The project the epic belongs to. | | `data.reference` | string | yes | The epic's key. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.title` | string | yes | What the epic is called after the restore. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC3", "type": "epic.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-12", "restored_from_version": 2, "title": "Loyalty tiers", "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # epic.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-status-changed An epic moves along its lifecycle. ### When this fires A person moves the epic from the Epics page, or a token calls the status endpoint. The payload carries the status before and after, so a listener that mirrors the agreed scope adds the epic when `status` becomes `approved` and drops it when it becomes `cancelled`. ### Related events - `epic.updated`: a change to the record itself. - `epic.deleted`: the terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `epic.status_changed` | yes | The event name, always `epic.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.epic_id` | string (uuid) | yes | The epic's id. | | `data.previous_status` | `draft`, `in_review`, `approved`, `in_progress`, `done`, `cancelled` | yes | Where the epic was. | | `data.project_id` | string (uuid) | yes | The project the epic belongs to. | | `data.reference` | string | yes | The epic's key. | | `data.status` | `draft`, `in_review`, `approved`, `in_progress`, `done`, `cancelled` | yes | Where the epic is now. | | `data.title` | string | yes | What the epic is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC2", "type": "epic.status_changed", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "previous_status": "in_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-12", "status": "approved", "title": "Loyalty tiers" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # epic.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-updated An epic's fields change. ### When this fires Any of the epic's own columns change: the title, the prose, the owner, the priority, the dates or the colour. A status move raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after. ### Related events - `epic.status_changed`: a move along the lifecycle, which is deliberately not an update. - `epic.restored`: an earlier version put back, which is deliberately not an update either. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `epic.updated` | yes | The event name, always `epic.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.epic_id` | string (uuid) | yes | The epic's id. | | `data.project_id` | string (uuid) | yes | The project the epic belongs to. | | `data.reference` | string | yes | The epic's key. | | `data.title` | string | yes | What the epic is called after the change. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VC1", "type": "epic.updated", "created_at": "2026-10-28T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "priority": { "from": "should", "to": "must" }, "due_on": { "from": null, "to": "2027-03-31" } }, "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-12", "title": "Loyalty tiers" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # epic.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic The lifecycle of a project's epics: an epic added, changed, moved along its lifecycle, put back to an earlier version and deleted. An epic is the scope a release is cut from and the parent of every story, so a mirror of the planning record, a roadmap, a reporting tool and the client's own view subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the reference and what the transition itself carries, never a snapshot of the epic. Read the [epics endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/epics) for the record and its versions. ## Required ability Tokens subscribing to `epic.*` events need `epic:view` at mint time. ## Events - [`epic.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-created) — An epic is added. - [`epic.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-updated) — An epic's fields change. - [`epic.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-status-changed) — An epic moves along its lifecycle. - [`epic.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-restored) — An earlier version of an epic is put back. - [`epic.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-deleted) — An epic is deleted. --- # glossary_term.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-created A term is added. ### When this fires A person adds a term on the Glossary page or in the intake wizard, or a token creates one through the API. The payload carries the id other terms will name among their related terms and the status the term starts in. ### Related events - `glossary_term.updated`: the next change. - `glossary_term.status_changed`: the term's review. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `glossary_term.created` | yes | The event name, always `glossary_term.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.glossary_term_id` | string (uuid) | yes | The term's id. | | `data.project_id` | string (uuid) | yes | The project the term belongs to. | | `data.status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the term starts: `draft` unless the creator said otherwise. | | `data.term` | string | yes | The word, unique within the project. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VB0", "type": "glossary_term.created", "created_at": "2026-10-20T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "glossary_term_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "status": "draft", "term": "Branch" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # glossary_term.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-deleted A term is deleted. ### When this fires A person deletes the term from the Glossary page, or a token calls the delete endpoint. The row is gone and the terms that leaned on it have dropped the relation; the payload carries what a mirror needs to drop its own. ### Related events - `glossary_term.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `glossary_term.deleted` | yes | The event name, always `glossary_term.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.glossary_term_id` | string (uuid) | yes | The term's id. | | `data.project_id` | string (uuid) | yes | The project the term belonged to. | | `data.term` | string | yes | The word. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VB3", "type": "glossary_term.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "glossary_term_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "term": "Basket" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # glossary_term.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-restored An earlier version of a term is put back. ### When this fires A person restores a version from the term's history on the Glossary tab, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, and the payload carries both numbers. ### Related events - `glossary_term.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `glossary_term.restored` | yes | The event name, always `glossary_term.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.glossary_term_id` | string (uuid) | yes | The term's id. | | `data.project_id` | string (uuid) | yes | The project the term belongs to. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.term` | string | yes | The term after the restore. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VKC", "type": "glossary_term.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "glossary_term_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "restored_from_version": 2, "term": "Basket", "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # glossary_term.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-status-changed A term moves along its review. ### When this fires A person moves the term from the Glossary page, or a token calls the status endpoint. The payload carries the status before and after, so a listener that mirrors the approved glossary adds the term when `status` becomes `approved` and drops it when `previous_status` was. ### Related events - `glossary_term.updated`: a change to the words themselves. - `glossary_term.deleted`: the terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `glossary_term.status_changed` | yes | The event name, always `glossary_term.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.glossary_term_id` | string (uuid) | yes | The term's id. | | `data.previous_status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the term was. | | `data.project_id` | string (uuid) | yes | The project the term belongs to. | | `data.status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the term is now. | | `data.term` | string | yes | The word. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VB2", "type": "glossary_term.status_changed", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "glossary_term_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "previous_status": "in_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "status": "approved", "term": "Branch" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # glossary_term.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-updated A term's fields change. ### When this fires Any of the term's own columns change: the word, the definition, the aliases, the related terms or the source. A status move raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after; the aliases and the related ids arrive as lists. ### Related events - `glossary_term.status_changed`: a move along the review, which is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `glossary_term.updated` | yes | The event name, always `glossary_term.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`; the aliases and the related ids as lists. | | `data.glossary_term_id` | string (uuid) | yes | The term's id. | | `data.project_id` | string (uuid) | yes | The project the term belongs to. | | `data.term` | string | yes | The word after the change. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VB1", "type": "glossary_term.updated", "created_at": "2026-10-27T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "aliases": { "from": [ "Site" ], "to": [ "Site", "Location" ] }, "source": { "from": null, "to": "Client operations manual, 2025 edition" } }, "glossary_term_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "term": "Branch" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # glossary_term.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term The lifecycle of a project's glossary: a term added, changed, moved along its review and deleted. The glossary is the project's own language, read by every story, requirement and test, by Heimdall and by the agents that draft in it, so a mirror of the planning record subscribes to this family to keep its words current. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the word and what the transition itself carries, never a snapshot of the term. Read the [glossary endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/glossary) for the record. ## Required ability Tokens subscribing to `glossary_term.*` events need `glossary:view` at mint time. ## Events - [`glossary_term.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-created) — A term is added. - [`glossary_term.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-updated) — A term's fields change. - [`glossary_term.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-status-changed) — A term moves along its review. - [`glossary_term.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-deleted) — A term is deleted. - [`glossary_term.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-restored) — An earlier version of a term is put back. --- # import.discarded Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-discarded A person dropped the batch. ### When this fires A person presses Discard on the Brief Import page, or a token posts to the batch's discard endpoint. The suggestions stay readable but can no longer be decided. ### Related events - `import.started`: the beginning of the batch. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `import.discarded` | yes | The event name, always `import.discarded` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.batch_id` | string (uuid) | yes | The batch's id. | | `data.project_id` | string (uuid) | yes | The project the brief was imported into. | | `data.title` | string | yes | What the batch was called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VM4", "type": "import.discarded", "created_at": "2026-10-28T13:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "batch_id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f50", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "title": "Customer portal brief" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # import.failed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-failed The reading failed. ### When this fires The queue could not read the brief: the model did not answer, the team's monthly AI budget had no room, or the person who started the import is no longer a member. The batch is left failed with the reason, and a person starts it again on purpose. ### Related events - `import.started`: a person starts the brief again. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `import.failed` | yes | The event name, always `import.failed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.batch_id` | string (uuid) | yes | The batch's id. | | `data.project_id` | string (uuid) | yes | The project the brief is imported into. | | `data.reason` | string | yes | Why it failed, as a person reads it. | | `data.title` | string | yes | What the batch is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VM3", "type": "import.failed", "created_at": "2026-10-28T12:02:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "batch_id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f50", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reason": "Heimdall could not reach its model just now. Nothing was changed; try again in a moment.", "title": "Customer portal brief" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # import.ready Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-ready Heimdall read the brief. ### When this fires The queue finished reading the brief and wrote the suggestions. The payload carries how many; read the batch for the suggestions themselves. ### Related events - `import.suggestion_accepted`: a person starts deciding. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `import.ready` | yes | The event name, always `import.ready` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.batch_id` | string (uuid) | yes | The batch's id. | | `data.project_id` | string (uuid) | yes | The project the brief is imported into. | | `data.suggestion_count` | integer | yes | How many suggestions Heimdall proposed. | | `data.title` | string | yes | What the batch is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VM2", "type": "import.ready", "created_at": "2026-10-28T12:02:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "batch_id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f50", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "suggestion_count": 24, "title": "Customer portal brief" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # import.started Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-started A brief is handed to Heimdall. ### When this fires A person presses Import on the Brief Import page, or a token posts a brief through the API. The batch is queued; the reading follows on the queue as the person who started it. ### Related events - `import.ready`: the reading is done. - `import.failed`: the reading failed. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `import.started` | yes | The event name, always `import.started` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.batch_id` | string (uuid) | yes | The batch's id. | | `data.project_id` | string (uuid) | yes | The project the brief is imported into. | | `data.source` | `pasted_text`, `upload`, `url`, `meeting_notes` | yes | Where the words came from. | | `data.title` | string | yes | What the batch is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VM1", "type": "import.started", "created_at": "2026-10-28T12:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "batch_id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f50", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "source": "pasted_text", "title": "Customer portal brief" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # import.suggestion_accepted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-accepted A person accepted a suggestion. ### When this fires A person presses Accept or saves the editor on the review queue, or a token posts to the suggestion's accept endpoint. For a record kind the record was written through its own door and announces its own `created` event; for an improvement, the note was kept and `artefact_id` is null. ### Related events - `import.suggestion_rejected`: the other decision. - `import.suggestion_merged`: the record already existed. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `import.suggestion_accepted` | yes | The event name, always `import.suggestion_accepted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.artefact_id` | string \| null (uuid) | yes | The record written; null for a note. | | `data.artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `glossary-term`, `persona`, `stakeholder`, `raid-item`, `decision`, null | yes | The record's alias; null for a note. | | `data.batch_id` | string \| null (uuid) | yes | The batch the suggestion came from; null for a note from a sweep. | | `data.edited` | boolean | yes | Whether the person changed the fields before accepting. | | `data.kind` | `epic`, `story`, `functional_requirement`, `non_functional_requirement`, `business_rule`, `use_case`, `glossary_term`, `persona`, `stakeholder`, `risk`, `assumption`, `decision`, `improvement`, `research` | yes | What the suggestion proposed. | | `data.project_id` | string (uuid) | yes | The project. | | `data.suggestion_id` | string (uuid) | yes | The suggestion's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VM5", "type": "import.suggestion_accepted", "created_at": "2026-10-28T12:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "artefact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "artefact_type": "epic", "batch_id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f50", "edited": false, "kind": "epic", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "suggestion_id": "e2f3a4b5-c6d7-4e8f-9a0b-1c2d3e4f5a61" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # import.suggestion_merged Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-merged A person merged a suggestion into a record the project already held. ### When this fires A person picks an existing record under Merge on the review queue, or a token posts to the suggestion's merge endpoint. The record is left as it is; the suggestion points at it. ### Related events - `import.suggestion_accepted`: the record did not exist yet. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `import.suggestion_merged` | yes | The event name, always `import.suggestion_merged` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.artefact_id` | string (uuid) | yes | The record the suggestion was merged into. | | `data.artefact_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `glossary-term`, `persona`, `stakeholder`, `raid-item`, `decision` | yes | The record's alias. | | `data.batch_id` | string \| null (uuid) | yes | The batch the suggestion came from; null for a note from a sweep. | | `data.kind` | `epic`, `story`, `functional_requirement`, `non_functional_requirement`, `business_rule`, `use_case`, `glossary_term`, `persona`, `stakeholder`, `risk`, `assumption`, `decision`, `improvement`, `research` | yes | What the suggestion proposed. | | `data.project_id` | string (uuid) | yes | The project. | | `data.suggestion_id` | string (uuid) | yes | The suggestion's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VM7", "type": "import.suggestion_merged", "created_at": "2026-10-28T12:32:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "artefact_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "artefact_type": "business-rule", "batch_id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f50", "kind": "business_rule", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "suggestion_id": "a4b5c6d7-e8f9-4a0b-8c1d-2e3f4a5b6c83" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # import.suggestion_rejected Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-rejected A person rejected a suggestion, or dismissed a note. ### When this fires A person presses Reject on the review queue, or a token posts to the suggestion's reject endpoint. Nothing was written to the project. ### Related events - `import.suggestion_accepted`: the other decision. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `import.suggestion_rejected` | yes | The event name, always `import.suggestion_rejected` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.batch_id` | string \| null (uuid) | yes | The batch the suggestion came from; null for a note from a sweep. | | `data.kind` | `epic`, `story`, `functional_requirement`, `non_functional_requirement`, `business_rule`, `use_case`, `glossary_term`, `persona`, `stakeholder`, `risk`, `assumption`, `decision`, `improvement`, `research` | yes | What the suggestion proposed. | | `data.project_id` | string (uuid) | yes | The project. | | `data.suggestion_id` | string (uuid) | yes | The suggestion's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VM6", "type": "import.suggestion_rejected", "created_at": "2026-10-28T12:31:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "batch_id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f50", "kind": "assumption", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "suggestion_id": "f3a4b5c6-d7e8-4f9a-8b1c-2d3e4f5a6b72" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # import.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import The life of a brief import: a brief handed to Heimdall, read into suggestions or failed, discarded, and each suggestion accepted, rejected or merged by a person. A dashboard that follows how a project's planning record is filling up, a bot that nudges the analyst when a batch is ready, and a mirror of the records accepted subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads carry ids, kinds and counts, never the brief or the fields of a suggestion: read the [imports endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/imports) for those. A record written by an accepted suggestion also announces its own `created` event, in its own family, with the ids a mirror needs. ## Required ability Tokens subscribing to `import.*` events need `import:view` at mint time. ## Events - [`import.started`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-started) — A brief is handed to Heimdall. - [`import.ready`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-ready) — Heimdall read the brief. - [`import.failed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-failed) — The reading failed. - [`import.discarded`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-discarded) — A person dropped the batch. - [`import.suggestion_accepted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-accepted) — A person accepted a suggestion. - [`import.suggestion_rejected`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-rejected) — A person rejected a suggestion, or dismissed a note. - [`import.suggestion_merged`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-merged) — A person merged a suggestion into a record the project already held. --- # improvement.swept Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/improvement/improvement-swept The sweep ran on a project. ### When this fires The weekly sweep reached the project, a brief import on the project was fully decided, a person pressed the sweep on the dashboard rail, or a token posted to the project's improvements endpoint. The counts say what changed on the review queue; a sweep that found nothing new still fires, with zero proposed. ### Related events - `import.suggestion_accepted`: a person keeps one of the cards. - `import.suggestion_rejected`: a person dismisses one, and it is never proposed again. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `improvement.swept` | yes | The event name, always `improvement.swept` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.pending` | integer | yes | How many sweep cards wait for a decision after this sweep. | | `data.project_id` | string (uuid) | yes | The project swept. | | `data.proposed` | integer | yes | How many gaps became new improvement cards. | | `data.retired` | integer | yes | How many pending cards were taken down because their gap was filled. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VN4", "type": "improvement.swept", "created_at": "2026-10-28T06:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "pending": 5, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "proposed": 3, "retired": 1 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # improvement.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/improvement Heimdall's improvement sweep of a project's record: one event each time it ran, with what it proposed, took down and left waiting. A dashboard that follows how complete a project's planning record is subscribes to this family; the cards themselves are read through the project's suggestions endpoint. The event is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's sweeps. The payload carries counts, never the cards: read the [suggestions endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/research) for those, and decide them with the `import.suggestion_*` events' endpoints. ## Required ability Tokens subscribing to `improvement.*` events need `import:view` at mint time. ## Events - [`improvement.swept`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/improvement/improvement-swept) — The sweep ran on a project. --- # Event Reference Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events Every outbound webhook event, generated from the same OpenAPI document as the API reference and grouped by family: when it fires, the signed envelope and its headers, the `data` payload field by field, named examples, caveats and related events. Each family has an overview page carrying the shared narrative and the ability a token needs to subscribe an endpoint to its events, and one page per event; a row below opens the event's page. ## `ai.*` events The team's monthly AI budget approaching and reaching its cap. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai). - [`ai.budget_warning`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai/ai-budget-warning) — The month's spend crosses the budget's alert threshold. - [`ai.budget_exhausted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/ai/ai-budget-exhausted) — The month's spend reaches the cap. ## `approval.*` events The life of an approval request: an artefact put up for approval, each approver's verdict, the outcome the verdicts settle, a request taken back, a request that ran past its time, and a change to how a project decides a kind of approval. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval). - [`approval.requested`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-requested) — An artefact is put up for approval. - [`approval.decided`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-decided) — One approver gives a verdict. - [`approval.approved`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-approved) — A request carries. - [`approval.rejected`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-rejected) — An approver rejects the request. - [`approval.changes_requested`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-changes-requested) — An approver asks for changes. - [`approval.withdrawn`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-withdrawn) — An open request is taken back. - [`approval.escalated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-escalated) — A pending request passes the time the policy allows. - [`approval.policy_updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/approval/approval-policy-updated) — A project changes how one kind of approval is decided. ## `attachment.*` events The files pinned on a project's artefacts: a file uploaded and a file removed. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment). - [`attachment.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment/attachment-created) — A file is uploaded onto an artefact. - [`attachment.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/attachment/attachment-deleted) — A file is removed. ## `baseline.*` events The life of a project's baselines: a baseline added as a draft, its words or items changed, moved by hand, frozen by the team's approval, signed by the client, put back to an earlier version and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline). - [`baseline.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-created) — A baseline is added. - [`baseline.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-updated) — A draft baseline's words or records change. - [`baseline.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-status-changed) — A baseline moves between its states. - [`baseline.frozen`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-frozen) — The team's approval froze a baseline. - [`baseline.signed_off`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-signed-off) — The client signed a scope baseline. - [`baseline.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-restored) — An earlier version of a draft baseline is put back. - [`baseline.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/baseline/baseline-deleted) — A draft baseline is deleted. ## `business_rule.*` events The lifecycle of a project's business rules: a rule added, changed, moved along its review lifecycle, put back to an earlier version and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule). - [`business_rule.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-created) — A rule is added. - [`business_rule.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-updated) — A rule's fields change. - [`business_rule.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-status-changed) — A rule moves along its review lifecycle. - [`business_rule.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-restored) — An earlier version of a rule is put back. - [`business_rule.quality_reviewed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-quality-reviewed) — A rule's quality is reviewed. - [`business_rule.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/business-rule/business-rule-deleted) — A rule is deleted. ## `change_request.*` events The life of a project's change requests: a request raised, its words or impact changed, moved along its lifecycle by hand or by the approval engine, read by Heimdall for its impact, put back to an earlier version and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request). - [`change_request.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-created) — A change request is raised. - [`change_request.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-updated) — A change request's fields change. - [`change_request.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-status-changed) — A change request moves between its states. - [`change_request.analysed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-analysed) — Heimdall read a change request against its baseline. - [`change_request.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-restored) — An earlier version of a change request is put back. - [`change_request.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/change-request/change-request-deleted) — A change request that was never agreed is deleted. ## `client.*` events The lifecycle of a client: creation, edits, archiving, restoring and deletion. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client). - [`client.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-created) — A client is added. - [`client.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-updated) — A client's fields change. - [`client.archived`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-archived) — A client is archived. - [`client.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-restored) — An archived client is restored. - [`client.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/client/client-deleted) — A client that no project names is deleted. ## `comment.*` events The conversations on a project's artefacts: a comment posted, edited, resolved, reopened and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment). - [`comment.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-created) — A comment is posted. - [`comment.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-updated) — A comment's words are rewritten. - [`comment.resolved`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-resolved) — The first comment of a thread is marked resolved. - [`comment.reopened`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-reopened) — A resolved thread is marked open again. - [`comment.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/comment/comment-deleted) — A comment is deleted. ## `component.*` events The lifecycle of a project's components: creation, edits, archiving, restoring and deletion. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component). - [`component.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-created) — A component is added. - [`component.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-updated) — A component's fields change. - [`component.archived`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-archived) — A component is archived. - [`component.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-restored) — An archived component is restored. - [`component.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/component/component-deleted) — A component no environment runs is deleted. ## `council.*` events [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council). - [`council.convened`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-convened) — council.convened - [`council.voice_spoke`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-voice-spoke) — council.voice_spoke - [`council.round_opened`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-round-opened) — council.round_opened - [`council.ready`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-ready) — council.ready - [`council.failed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-failed) — council.failed - [`council.finding_accepted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-accepted) — council.finding_accepted - [`council.finding_dismissed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/council/council-finding-dismissed) — council.finding_dismissed ## `data_entity.*` events The lifecycle of a project's data dictionary: an entity created, changed, moved along its review, its fields replaced and the entity deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity). - [`data_entity.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-created) — An entity is created. - [`data_entity.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-updated) — An entity's fields change. - [`data_entity.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-status-changed) — An entity moves along its review. - [`data_entity.fields_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-fields-changed) — The entity's fields are replaced. - [`data_entity.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-deleted) — An entity is deleted. - [`data_entity.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/data-entity/data-entity-restored) — An earlier version of a entity is put back. ## `decision.*` events The lifecycle of a project's decisions: a decision added, changed, moved along its lifecycle, put back to an earlier version and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision). - [`decision.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-created) — A decision is added. - [`decision.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-updated) — A decision's fields change. - [`decision.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-status-changed) — A decision moves along its lifecycle. - [`decision.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-restored) — An earlier version of a decision is put back. - [`decision.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/decision/decision-deleted) — A decision is deleted. ## `design_reference.*` events The lifecycle of a project's design references: a link added, changed and removed. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference). - [`design_reference.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-created) — A reference is added. - [`design_reference.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-updated) — A reference's fields change. - [`design_reference.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/design-reference/design-reference-deleted) — A reference is deleted. ## `environment.*` events The lifecycle and the state of a project's environments: creation, edits, deletion, change freezes and the health the checks find. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment). - [`environment.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-created) — An environment is added. - [`environment.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-updated) — An environment's fields change. - [`environment.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-deleted) — An environment is deleted. - [`environment.frozen`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-frozen) — A change freeze starts. - [`environment.unfrozen`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-unfrozen) — A change freeze is lifted before it ends. - [`environment.health_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/environment/environment-health-changed) — A check finds the environment in a different health than before. ## `epic.*` events The lifecycle of a project's epics: an epic added, changed, moved along its lifecycle, put back to an earlier version and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic). - [`epic.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-created) — An epic is added. - [`epic.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-updated) — An epic's fields change. - [`epic.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-status-changed) — An epic moves along its lifecycle. - [`epic.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-restored) — An earlier version of an epic is put back. - [`epic.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/epic/epic-deleted) — An epic is deleted. ## `glossary_term.*` events The lifecycle of a project's glossary: a term added, changed, moved along its review and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term). - [`glossary_term.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-created) — A term is added. - [`glossary_term.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-updated) — A term's fields change. - [`glossary_term.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-status-changed) — A term moves along its review. - [`glossary_term.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-deleted) — A term is deleted. - [`glossary_term.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/glossary-term/glossary-term-restored) — An earlier version of a term is put back. ## `import.*` events The life of a brief import: a brief handed to Heimdall, read into suggestions or failed, discarded, and each suggestion accepted, rejected or merged by a person. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import). - [`import.started`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-started) — A brief is handed to Heimdall. - [`import.ready`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-ready) — Heimdall read the brief. - [`import.failed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-failed) — The reading failed. - [`import.discarded`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-discarded) — A person dropped the batch. - [`import.suggestion_accepted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-accepted) — A person accepted a suggestion. - [`import.suggestion_rejected`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-rejected) — A person rejected a suggestion, or dismissed a note. - [`import.suggestion_merged`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/import/import-suggestion-merged) — A person merged a suggestion into a record the project already held. ## `improvement.*` events Heimdall's improvement sweep of a project's record: one event each time it ran, with what it proposed, took down and left waiting. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/improvement). - [`improvement.swept`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/improvement/improvement-swept) — The sweep ran on a project. ## `label.*` events A project's label vocabulary: a label added, rewritten and removed. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label). - [`label.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-created) — A word is added to the vocabulary. - [`label.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-updated) — A label's word, colour or note changes. - [`label.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-deleted) — A label is removed. ## `link.*` events The links between a project's artefacts: a link made and a link removed. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link). - [`link.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-created) — Two artefacts are linked. - [`link.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-deleted) — A link is removed. ## `persona.*` events The lifecycle of a project's personas: creation, edits, archiving, restoring and deletion. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona). - [`persona.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-created) — A persona is added. - [`persona.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-updated) — A persona's fields change. - [`persona.archived`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-archived) — A persona is archived. - [`persona.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-restored) — An archived persona is restored. - [`persona.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-deleted) — A persona is deleted. - [`persona.version_restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-version-restored) — An earlier version of a persona is put back. ## `project.*` events The lifecycle of a project and its membership: creation, edits, status moves, archiving, restoring, deletion, and people joining or leaving it. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project). - [`project.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-created) — A project is created. - [`project.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-updated) — A project's own fields change. - [`project.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-status-changed) — A project moves between lifecycle statuses. - [`project.archived`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-archived) — A project is archived. - [`project.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-restored) — An archived project is restored. - [`project.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-deleted) — A project is deleted with everything under it. - [`project.settings_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-settings-changed) — One or more of the project's settings change. - [`project.member_added`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-member-added) — A person gains a membership row on the project. - [`project.member_removed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-member-removed) — A person's membership row on the project is removed. ## `raid_item.*` events The lifecycle of a project's RAID log: a risk, an assumption, an issue or a dependency raised, changed, moved along its lifecycle, put back to an earlier version and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item). - [`raid_item.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-created) — An item is raised. - [`raid_item.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-updated) — An item's fields change. - [`raid_item.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-status-changed) — An item moves along its lifecycle. - [`raid_item.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-restored) — An earlier version of an item is put back. - [`raid_item.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-deleted) — An item is deleted. ## `repository.*` events The lifecycle of a project's repositories: registration, edits, disconnecting, reconnecting and deletion. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository). - [`repository.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-created) — A repository is registered. - [`repository.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-updated) — A repository's fields change. - [`repository.disconnected`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-disconnected) — A repository is set aside. - [`repository.reconnected`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-reconnected) — A disconnected repository is brought back. - [`repository.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-deleted) — A repository is deleted. ## `requirement.*` events The lifecycle of a project's requirements: a functional or non-functional requirement added, changed, moved along its review lifecycle, put back to an earlier version and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement). - [`requirement.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-created) — A requirement is added. - [`requirement.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-updated) — A requirement's fields change. - [`requirement.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-status-changed) — A requirement moves along its review lifecycle. - [`requirement.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-restored) — An earlier version of a requirement is put back. - [`requirement.quality_reviewed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-quality-reviewed) — A requirement's quality is reviewed. - [`requirement.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-deleted) — A requirement is deleted. ## `research.*` events The life of a research run: Heimdall asked to read a project's domain, done with notes and suggestions, or failed. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research). - [`research.started`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-started) — A person asks Heimdall to research the project. - [`research.ready`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-ready) — Heimdall finished the research. - [`research.failed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-failed) — The run failed. ## `role.*` events Role creation, edits and deletion. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role). - [`role.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-created) — A role is created. - [`role.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-updated) — A role's name, description or permissions change. - [`role.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-deleted) — A custom role is deleted. ## `stakeholder.*` events The lifecycle of a project's stakeholders: who was added to the record, what changed about them and who was removed. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder). - [`stakeholder.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-created) — A stakeholder is added. - [`stakeholder.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-updated) — A stakeholder's fields change. - [`stakeholder.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-deleted) — A stakeholder is removed. - [`stakeholder.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-restored) — An earlier version of a stakeholder is put back. ## `team.*` events Team creation, rename and deletion, and the comings and goings of the people in it. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team). - [`team.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-created) — A team is created. - [`team.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-updated) — A team is renamed. - [`team.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-deleted) — A team with no projects is deleted. - [`team.member_invited`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-invited) — An invitation is sent to a staff member or a client guest. - [`team.member_joined`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-joined) — An invitee accepts and becomes a member. - [`team.member_removed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-removed) — A member is removed from the team. - [`team.member_role_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-role-changed) — A member moves to another of the team's roles. ## `token.*` events Personal access token minting and revocation. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token). - [`token.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token/token-created) — A token is minted. - [`token.revoked`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token/token-revoked) — A token is revoked. ## `use_case.*` events The lifecycle of a project's use cases: a use case added, changed, moved along its review lifecycle, given new steps, put back to an earlier version and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case). - [`use_case.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-created) — A use case is added. - [`use_case.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-updated) — A use case's fields change. - [`use_case.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-status-changed) — A use case moves along its review lifecycle. - [`use_case.steps_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-steps-changed) — A use case's steps are replaced. - [`use_case.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-restored) — An earlier version of a use case is put back. - [`use_case.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-deleted) — A use case is deleted. ## `work_item.*` events The lifecycle of a project's work items: a story, task, sub-task, bug, spike or improvement added, changed, moved along its lifecycle, handed to a person, given new acceptance criteria, put back to an earlier version and deleted. [Read the overview](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item). - [`work_item.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-created) — An item is added. - [`work_item.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-updated) — An item's fields change. - [`work_item.transitioned`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-transitioned) — An item moves along its lifecycle. - [`work_item.assigned`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-assigned) — An item is handed to a person, or to nobody. - [`work_item.criteria_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-criteria-changed) — An item's acceptance criteria are replaced. - [`work_item.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-restored) — An earlier version of an item is put back. - [`work_item.quality_reviewed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-quality-reviewed) — An item's quality is reviewed. - [`work_item.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-deleted) — An item is deleted. --- # label.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label A project's label vocabulary: a label added, rewritten and removed. A board that mirrors the backlog's colours and a report that groups by label subscribe to this family; which items carry which label travels with the work item events. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads carry the word and the colour, enough to redraw a chip without reading the label back. ## Required ability Tokens subscribing to `label.*` events need `work-item:view` at mint time. ## Events - [`label.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-created) — A word is added to the vocabulary. - [`label.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-updated) — A label's word, colour or note changes. - [`label.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-deleted) — A label is removed. --- # label.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-created A word is added to the vocabulary. ### When this fires A person adds a label from the backlog's label manager, or a token creates one through the API. ### Related events - `label.updated`: the word or the colour changes. - `label.deleted`: the end of the label. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `label.created` | yes | The event name, always `label.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` | yes | The badge colour. | | `data.label_id` | string (uuid) | yes | The label's id. | | `data.name` | string | yes | The word. | | `data.project_id` | string (uuid) | yes | The project whose vocabulary it joins. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK7", "type": "label.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "color": "amber", "label_id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01", "name": "payments", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # label.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-deleted A label is removed. ### When this fires A person removes a label from the label manager, or a token deletes it. The label is unpinned from every item; the items themselves are untouched and fire nothing. ### Related events - `label.created`: the label's beginning. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `label.deleted` | yes | The event name, always `label.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` | yes | The badge colour it had. | | `data.label_id` | string (uuid) | yes | The label's id. | | `data.name` | string | yes | The word it was. | | `data.project_id` | string (uuid) | yes | The project whose vocabulary it left. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK9", "type": "label.deleted", "created_at": "2026-11-02T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "color": "rose", "label_id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01", "name": "payments", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # label.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/label/label-updated A label's word, colour or note changes. ### When this fires A person rewrites a label from the label manager, or a token patches it. A save that changes nothing fires nothing. The payload carries the new word and colour. ### Related events - `label.created`: the label's beginning. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `label.updated` | yes | The event name, always `label.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.color` | `zinc`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` | yes | The badge colour, after the change. | | `data.label_id` | string (uuid) | yes | The label's id. | | `data.name` | string | yes | The word, after the change. | | `data.project_id` | string (uuid) | yes | The project whose vocabulary it belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK8", "type": "label.updated", "created_at": "2026-10-29T08:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "color": "rose", "label_id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01", "name": "payments", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # link.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link The links between a project's artefacts: a link made and a link removed. A traceability mirror, a wiki that draws the dependency graph and the people's notifications subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads carry the two ends by type and id and the kind, never the artefacts themselves. Read the [links endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/links) for both ends' keys and titles. ## Required ability Tokens subscribing to `link.*` events need `link:view` at mint time. ## Events - [`link.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-created) — Two artefacts are linked. - [`link.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-deleted) — A link is removed. --- # link.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-created Two artefacts are linked. ### When this fires A person links two artefacts from either one's detail flyout, an agent links them with `link_artefacts`, or a token creates the link through the API. The payload carries both ends and the kind read from the source. ### Related events - `link.deleted`: the end of the edge. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `link.created` | yes | The event name, always `link.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.kind` | `implements`, `derives_from`, `verifies`, `covers`, `depends_on`, `blocks`, `relates_to`, `duplicates`, `caused_by`, `fixed_by`, `supersedes` | yes | How the source relates to the target. | | `data.link_id` | string (uuid) | yes | The link's id. | | `data.project_id` | string (uuid) | yes | The project both artefacts belong to. | | `data.source_id` | string (uuid) | yes | The source artefact's id. | | `data.source_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The source artefact's type. | | `data.target_id` | string (uuid) | yes | The target artefact's id. | | `data.target_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The target artefact's type. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK0", "type": "link.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "kind": "implements", "link_id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f56", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "source_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "source_type": "work-item", "target_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "target_type": "requirement" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # link.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/link/link-deleted A link is removed. ### When this fires A person unlinks two artefacts from either one's detail flyout, or a token calls the delete endpoint. The payload carries what the link joined, so a mirror can drop its own edge. ### Related events - `link.created`: the beginning of the edge. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `link.deleted` | yes | The event name, always `link.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.kind` | `implements`, `derives_from`, `verifies`, `covers`, `depends_on`, `blocks`, `relates_to`, `duplicates`, `caused_by`, `fixed_by`, `supersedes` | yes | How the source related to the target. | | `data.link_id` | string (uuid) | yes | The link's id. | | `data.project_id` | string (uuid) | yes | The project both artefacts belong to. | | `data.source_id` | string (uuid) | yes | The source artefact's id. | | `data.source_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The source artefact's type. | | `data.target_id` | string (uuid) | yes | The target artefact's id. | | `data.target_type` | `epic`, `work-item`, `requirement`, `business-rule`, `use-case`, `raid-item`, `decision`, `data-entity`, `glossary-term` | yes | The target artefact's type. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VK1", "type": "link.deleted", "created_at": "2026-11-02T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "kind": "implements", "link_id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f56", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "source_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "source_type": "work-item", "target_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "target_type": "requirement" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # persona.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona The lifecycle of a project's personas: creation, edits, archiving, restoring and deletion. A persona is an archetypal user the project is built for, named by its stories and use cases, so a mirror of the planning record subscribes to this family to keep the "as a …" of every story resolvable. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the name and what the transition itself carries, never a snapshot of the persona. Read the [personas endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/personas) for the record. ## Required ability Tokens subscribing to `persona.*` events need `persona:view` at mint time. ## Events - [`persona.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-created) — A persona is added. - [`persona.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-updated) — A persona's fields change. - [`persona.archived`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-archived) — A persona is archived. - [`persona.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-restored) — An archived persona is restored. - [`persona.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-deleted) — A persona is deleted. - [`persona.version_restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-version-restored) — An earlier version of a persona is put back. --- # persona.archived Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-archived A persona is archived. ### When this fires A person archives the persona from the Personas page, or a token calls the archive endpoint. The stories and use cases that name it keep resolving; it leaves the live listing. ### Related events - `persona.restored`: the way back. - `persona.deleted`: the terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `persona.archived` | yes | The event name, always `persona.archived` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The persona's name. | | `data.persona_id` | string (uuid) | yes | The persona's id. | | `data.project_id` | string (uuid) | yes | The project the persona belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VA2", "type": "persona.archived", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "name": "Occasional Browser", "persona_id": "c9d0e1f2-a3b4-4c5d-8e6f-7a8b9c0d1e23", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # persona.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-created A persona is added. ### When this fires A person adds a persona on the Personas page or in the intake wizard, or a token creates one through the API. The payload carries the id a story's `persona_id` will name. ### Related events - `persona.updated`: the next change. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `persona.created` | yes | The event name, always `persona.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The persona's name, unique within the project. | | `data.persona_id` | string (uuid) | yes | The persona's id. | | `data.project_id` | string (uuid) | yes | The project the persona belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VA0", "type": "persona.created", "created_at": "2026-10-20T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "name": "Busy Buyer", "persona_id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d12", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # persona.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-deleted A persona is deleted. ### When this fires A person deletes the persona from the Personas page, or a token calls the delete endpoint. The row is gone; the payload carries what a mirror needs to drop its own. ### Related events - `persona.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `persona.deleted` | yes | The event name, always `persona.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The persona's name. | | `data.persona_id` | string (uuid) | yes | The persona's id. | | `data.project_id` | string (uuid) | yes | The project the persona belonged to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VA4", "type": "persona.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "name": "Occasional Browser", "persona_id": "c9d0e1f2-a3b4-4c5d-8e6f-7a8b9c0d1e23", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # persona.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-restored An archived persona is restored. ### When this fires A person restores the persona from the Personas page, or a token calls the restore endpoint. It is back in the live listing. ### Related events - `persona.archived`: the state it came back from. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `persona.restored` | yes | The event name, always `persona.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The persona's name. | | `data.persona_id` | string (uuid) | yes | The persona's id. | | `data.project_id` | string (uuid) | yes | The project the persona belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VA3", "type": "persona.restored", "created_at": "2026-11-06T09:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "name": "Occasional Browser", "persona_id": "c9d0e1f2-a3b4-4c5d-8e6f-7a8b9c0d1e23", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # persona.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-updated A persona's fields change. ### When this fires Any of the persona's own columns change: its name, tagline, role title, the four prose fields, its tech proficiency, devices or quote. Archiving raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after; the devices arrive as a list. ### Related events - `persona.archived`: archiving, which is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `persona.updated` | yes | The event name, always `persona.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`; the devices as a list of tags. | | `data.name` | string | yes | The persona's name after the change. | | `data.persona_id` | string (uuid) | yes | The persona's id. | | `data.project_id` | string (uuid) | yes | The project the persona belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VA1", "type": "persona.updated", "created_at": "2026-10-27T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "devices": { "from": [ "Laptop", "Phone" ], "to": [ "Laptop", "Phone", "Tablet" ] }, "quote": { "from": null, "to": "Just let me repeat last month's order." } }, "name": "Busy Buyer", "persona_id": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d12", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # persona.version_restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/persona/persona-version-restored An earlier version of a persona is put back. ### When this fires A person restores a version from the persona's history on the Personas tab, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, and the payload carries both numbers. ### Related events - `persona.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `persona.version_restored` | yes | The event name, always `persona.version_restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The persona's name after the restore. | | `data.persona_id` | string (uuid) | yes | The persona's id. | | `data.project_id` | string (uuid) | yes | The project the persona belongs to. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VKC", "type": "persona.version_restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "name": "Busy Buyer", "persona_id": "e4f5a6b7-c8d9-4e0f-9a1b-2c3d4e5f6a81", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "restored_from_version": 2, "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # project.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project The lifecycle of a project and its membership: creation, edits, status moves, archiving, restoring, deletion, and people joining or leaving it. A project is the container every component, environment, epic, story, test, release and incident lives under, so these events are the ones a mirror of the registry subscribes to first. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: ids plus what the transition itself carries, never a snapshot of the project. Read the [projects endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/projects) for the record. > **Arriving with project membership.** Every event in the family is raised today except `project.member_removed`, which is declared, documented and delivered the moment the project membership page lands; nothing about its shape will change. A webhook test-fire borrows `project.created`'s shape with a `synthetic` marker. ## Required ability Tokens subscribing to `project.*` events need `project:view` at mint time, and `project-member:view` for the two `project.member_*` events. ## Events - [`project.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-created) — A project is created. - [`project.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-updated) — A project's own fields change. - [`project.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-status-changed) — A project moves between lifecycle statuses. - [`project.archived`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-archived) — A project is archived. - [`project.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-restored) — An archived project is restored. - [`project.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-deleted) — A project is deleted with everything under it. - [`project.settings_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-settings-changed) — One or more of the project's settings change. - [`project.member_added`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-member-added) — A person gains a membership row on the project. - [`project.member_removed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-member-removed) — A person's membership row on the project is removed. --- # project.archived Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-archived A project is archived. ### When this fires Someone archives the project. It leaves the registry's default view and its status reads `archived`, but every artefact under it is kept and readable; restoring brings it back as it was. ### Related events - `project.restored`: the way back. - `project.deleted`: the terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `project.archived` | yes | The event name, always `project.archived` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.key` | string | yes | The project key. | | `data.name` | string | yes | The project's name. | | `data.project_id` | string (uuid) | yes | The project. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V5Z", "type": "project.archived", "created_at": "2027-03-01T16:40:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "key": "CRM", "name": "Customer Portal", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # project.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-created A project is created. ### When this fires A person starts a project in the intake wizard, or a token creates one through the API; both come through the same door. The payload carries the identity a mirror needs to open the project and the three facts that decide how it is run. ### Related events - `project.status_changed`: the next move in the lifecycle. - `project.member_added`: the people who join it. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `project.created` | yes | The event name, always `project.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.client_id` | string \| null (uuid) | yes | The client the project is for; null for an internal project. | | `data.key` | string | yes | The project key, two to six upper-case letters and digits, the prefix of every reference such as `CRM-142`. | | `data.methodology` | `scrum`, `kanban`, `hybrid`, `waterfall` | yes | How the project is run. | | `data.name` | string | yes | The project's name, unique within the team. | | `data.project_id` | string (uuid) | yes | The project's id, the same value as the envelope's `project_id`. | | `data.status` | `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold`, `archived` | yes | The lifecycle status the project starts in, `discovery` unless the wizard was told otherwise. | | `data.visibility` | `team`, `members_only` | yes | `team` when every member of the team sees it, `members_only` when only its project members do. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V5W", "type": "project.created", "created_at": "2026-11-02T10:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "client_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "key": "CRM", "methodology": "scrum", "name": "Customer Portal", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "status": "discovery", "visibility": "team" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # project.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-deleted A project is deleted with everything under it. ### When this fires Someone deletes the project from the registry page, after typing its key to confirm, or through the API. Everything under it goes with it; there is no restore. The event is the last one the project's own endpoints receive, since an endpoint confined to the project is removed with it. ### Related events - `project.archived`: the reversible alternative. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `project.deleted` | yes | The event name, always `project.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.key` | string | yes | The project key, now free to be reused. | | `data.name` | string | yes | The project's last name. | | `data.project_id` | string (uuid) | yes | The project that no longer exists. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V61", "type": "project.deleted", "created_at": "2027-06-30T12:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "key": "CRM", "name": "Customer Portal", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # project.member_added Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-member-added A person gains a membership row on the project. ### When this fires A membership row is written for a person on the project: a guest accepting an invitation that named the project, or a staff member given a project role that overrides their team role. Staff who see the project through team visibility alone have no row and raise nothing. ### Related events - `team.member_joined`: fires in the same acceptance for a guest, since a guest is also a team member with the `guest` role. - `project.member_removed`: the row's removal. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `project.member_added` | yes | The event name, always `project.member_added` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.is_external` | boolean | yes | True for a client guest, false for staff with a project role override. | | `data.project_id` | string (uuid) | yes | The project. | | `data.role` | `owner`, `project_manager`, `business_analyst`, `developer`, `qa_engineer`, `release_manager`, `viewer`, `guest` | yes | The project role the row grants. | | `data.user_id` | string (uuid) | yes | The person. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V62", "type": "project.member_added", "created_at": "2026-11-03T14:22:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "is_external": true, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "role": "guest", "user_id": "4b8e2f6a-1c3d-4e5f-9a7b-8c9d0e1f2a3b" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # project.member_removed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-member-removed A person's membership row on the project is removed. ### When this fires A membership row is removed: a guest is taken off the project, or a staff member's project role override is dropped. Removing a person from the team removes their project rows as well and raises one event per row. ### Related events - `project.member_added`: predecessor. - `team.member_removed`: fires alongside when the person left the team. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `project.member_removed` | yes | The event name, always `project.member_removed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project. | | `data.user_id` | string (uuid) | yes | The person. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V63", "type": "project.member_removed", "created_at": "2027-01-15T10:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "user_id": "4b8e2f6a-1c3d-4e5f-9a7b-8c9d0e1f2a3b" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # project.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-restored An archived project is restored. ### When this fires Someone restores an archived project. It returns to the registry with the status it had before archiving. ### Related events - `project.archived`: what it undoes. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `project.restored` | yes | The event name, always `project.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.key` | string | yes | The project key. | | `data.name` | string | yes | The project's name. | | `data.project_id` | string (uuid) | yes | The project. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V60", "type": "project.restored", "created_at": "2027-03-04T08:10:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "key": "CRM", "name": "Customer Portal", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # project.settings_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-settings-changed One or more of the project's settings change. ### When this fires A person saves the project's Settings page, or a token patches the settings endpoint, and at least one key moved. `changes` holds one entry per setting that changed, keyed by the setting's name, each with the value before and after; a save that changes nothing raises nothing. ### Related events - `project.updated`: a change to the project's own fields rather than its settings. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `project.settings_changed` | yes | The event name, always `project.settings_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The settings that changed, keyed by name, each `{ "from": …, "to": … }`. | | `data.key` | string | yes | The project's key. | | `data.name` | string | yes | The project's name. | | `data.project_id` | string (uuid) | yes | The project's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VF0", "type": "project.settings_changed", "created_at": "2026-10-27T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "sprint_length_days": { "from": 14, "to": 10 }, "estimation_unit": { "from": "points", "to": "hours" } }, "key": "CRM", "name": "Customer Portal", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # project.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-status-changed A project moves between lifecycle statuses. ### When this fires The project's status changes, by hand on the registry page's detail flyout, inside an edit, or through the API. The statuses run `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold` and `archived`; archiving and restoring raise this event for the move and `project.archived` or `project.restored` as well, because a mirror may care about one and not the other. ### Related events - `project.archived`: the move into `archived` through the archive action. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `project.status_changed` | yes | The event name, always `project.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.key` | string | yes | The project key. | | `data.previous_status` | `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold`, `archived` | yes | The status the project left. | | `data.project_id` | string (uuid) | yes | The project. | | `data.status` | `discovery`, `planning`, `in_development`, `in_testing`, `live`, `maintenance`, `on_hold`, `archived` | yes | The status the project is in now. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V5Y", "type": "project.status_changed", "created_at": "2026-11-20T09:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "key": "CRM", "previous_status": "planning", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "status": "in_development" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # project.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/project/project-updated A project's own fields change. ### When this fires Any of the project's own columns change: its name, tagline, description, client, methodology, visibility, the four leads, the dates, the budget or a setting. A status move is not an update; it raises `project.status_changed` instead, and archiving raises its own event. ### Related events - `project.status_changed`: a status move, which is deliberately not an update. - `project.archived`: archiving, likewise. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `project.updated` | yes | The event name, always `project.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each mapped to an object with `from` and `to`. Only the changed columns appear. | | `data.key` | string | yes | The project key. | | `data.name` | string | yes | The project's name after the change. | | `data.project_id` | string (uuid) | yes | The project. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V5X", "type": "project.updated", "created_at": "2026-11-02T11:15:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "name": { "from": "Customer Portal", "to": "Customer Portal 2.0" }, "lead_pm_id": { "from": null, "to": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80" } }, "key": "CRM", "name": "Customer Portal 2.0", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # raid_item.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item The lifecycle of a project's RAID log: a risk, an assumption, an issue or a dependency raised, changed, moved along its lifecycle, put back to an earlier version and deleted. A project dashboard that counts what is open, a status report that lists the top risks and the people's notifications subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the reference, the kind and what the move itself carries, never a snapshot of the item. Read the [RAID log endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/raid-log) for the record and its versions. ## Required ability Tokens subscribing to `raid_item.*` events need `raid:view` at mint time. ## Events - [`raid_item.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-created) — An item is raised. - [`raid_item.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-updated) — An item's fields change. - [`raid_item.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-status-changed) — An item moves along its lifecycle. - [`raid_item.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-restored) — An earlier version of an item is put back. - [`raid_item.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-deleted) — An item is deleted. --- # raid_item.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-created An item is raised. ### When this fires A person raises an item on the RAID Log page, an agent raises a risk with `raise_risk`, or a token creates one through the API. The payload carries the reference the item was numbered with, its kind and the status it starts in. ### Related events - `raid_item.updated`: the next change. - `raid_item.status_changed`: the item's lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `raid_item.created` | yes | The event name, always `raid_item.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.raid_item_id` | string (uuid) | yes | The item's id. | | `data.reference` | string | yes | The key it was numbered with, such as `CRM-RSK-4`. | | `data.status` | `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred`, `accepted` | yes | Where it starts: `open` unless the creator said otherwise. | | `data.title` | string | yes | What the item is called. | | `data.type` | `risk`, `assumption`, `issue`, `dependency` | yes | The kind. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VH0", "type": "raid_item.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "raid_item_id": "f7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b02", "reference": "CRM-RSK-4", "status": "open", "title": "Payment provider sandbox may lag behind production", "type": "risk" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # raid_item.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-deleted An item is deleted. ### When this fires A person deletes the item from the RAID Log page, or a token calls the delete endpoint. The row and its versions are gone; the payload carries what a mirror needs to drop its own copy. ### Related events - `raid_item.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `raid_item.deleted` | yes | The event name, always `raid_item.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project the item belonged to. | | `data.raid_item_id` | string (uuid) | yes | The item's id. | | `data.reference` | string | yes | The item's key, never reused. | | `data.title` | string | yes | What the item was called. | | `data.type` | `risk`, `assumption`, `issue`, `dependency` | yes | The kind it was. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VH4", "type": "raid_item.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "raid_item_id": "f7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b02", "reference": "CRM-RSK-4", "title": "Payment provider sandbox may lag behind production", "type": "risk" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # raid_item.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-restored An earlier version of an item is put back. ### When this fires A person restores a version from the item's history, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, and the payload carries both numbers. ### Related events - `raid_item.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `raid_item.restored` | yes | The event name, always `raid_item.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.raid_item_id` | string (uuid) | yes | The item's id. | | `data.reference` | string | yes | The item's key. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.title` | string | yes | What the item is called after the restore. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VH3", "type": "raid_item.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "raid_item_id": "f7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b02", "reference": "CRM-RSK-4", "restored_from_version": 2, "title": "Payment provider sandbox may lag behind production", "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # raid_item.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-status-changed An item moves along its lifecycle. ### When this fires A person moves the item from the RAID Log page or the flyout, or a token calls the status endpoint. The payload carries the status before and after, so a dashboard that counts what is open reacts without diffing a column. ### Related events - `raid_item.deleted`: the end of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `raid_item.status_changed` | yes | The event name, always `raid_item.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.previous_status` | `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred`, `accepted` | yes | Where the item was. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.raid_item_id` | string (uuid) | yes | The item's id. | | `data.reference` | string | yes | The item's key. | | `data.status` | `open`, `monitoring`, `mitigating`, `resolved`, `closed`, `occurred`, `accepted` | yes | Where the item is now. | | `data.title` | string | yes | What the item is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VH2", "type": "raid_item.status_changed", "created_at": "2026-11-02T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "previous_status": "open", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "raid_item_id": "f7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b02", "reference": "CRM-RSK-4", "status": "monitoring", "title": "Payment provider sandbox may lag behind production" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # raid_item.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/raid-item/raid-item-updated An item's fields change. ### When this fires Any of the item's own columns change: the kind, the title, the description, the area, the probability or the impact, the mitigation or the contingency, the owner, the dates or what it depends on. A status move raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after; the derived score is among them when it moved. ### Related events - `raid_item.status_changed`: the move that is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `raid_item.updated` | yes | The event name, always `raid_item.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.raid_item_id` | string (uuid) | yes | The item's id. | | `data.reference` | string | yes | The item's key. | | `data.title` | string | yes | What the item is called after the change. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VH1", "type": "raid_item.updated", "created_at": "2026-10-29T10:15:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "probability": { "from": 3, "to": 4 }, "score": { "from": 12, "to": 16 } }, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "raid_item_id": "f7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b02", "reference": "CRM-RSK-4", "title": "Payment provider sandbox may lag behind production" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # repository.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository The lifecycle of a project's repositories: registration, edits, disconnecting, reconnecting and deletion. A repository is where a project's code lives and where branches, commits and pull requests will be read from once Git hosting is connected, so a pipeline, a mirror of the registry or a deployment tool subscribes to this family to follow which repositories are in use. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the full name and what the transition itself carries, never a snapshot of the repository. Read the [repositories endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/repositories) for the record. ## Required ability Tokens subscribing to `repository.*` events need `repository:view` at mint time. ## Events - [`repository.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-created) — A repository is registered. - [`repository.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-updated) — A repository's fields change. - [`repository.disconnected`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-disconnected) — A repository is set aside. - [`repository.reconnected`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-reconnected) — A disconnected repository is brought back. - [`repository.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-deleted) — A repository is deleted. --- # repository.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-created A repository is registered. ### When this fires A person registers a repository on the Repositories page or in the intake wizard, or a token creates one through the API. The payload carries the provider, so a listener that only cares about one host can act on it alone. ### Related events - `repository.updated`: the next change. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `repository.created` | yes | The event name, always `repository.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.full_name` | string | yes | The owner and the name joined, as the provider spells it. | | `data.project_id` | string (uuid) | yes | The project the repository belongs to. | | `data.provider` | `github`, `gitlab`, `bitbucket` | yes | Where the repository is hosted. | | `data.repository_id` | string (uuid) | yes | The repository's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE0", "type": "repository.created", "created_at": "2026-10-20T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "full_name": "zuse-technologies/customer-portal-web", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "provider": "github", "repository_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # repository.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-deleted A repository is deleted. ### When this fires A person deletes the repository from the Repositories page, or a token calls the delete endpoint. The row is gone; the code stays with the provider, and the payload carries what a mirror needs to drop its own row. ### Related events - `repository.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `repository.deleted` | yes | The event name, always `repository.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.full_name` | string | yes | The owner and the name joined. | | `data.project_id` | string (uuid) | yes | The project the repository belonged to. | | `data.repository_id` | string (uuid) | yes | The repository's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE4", "type": "repository.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "full_name": "zuse-technologies/legacy-importer", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "repository_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # repository.disconnected Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-disconnected A repository is set aside. ### When this fires A person disconnects the repository from the Repositories page, or a token calls the disconnect endpoint. The row and its history stay; the repository leaves the live listing and no branch, commit or pull request will be read from it until it is reconnected. ### Related events - `repository.reconnected`: the way back. - `repository.deleted`: the terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `repository.disconnected` | yes | The event name, always `repository.disconnected` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.full_name` | string | yes | The owner and the name joined. | | `data.project_id` | string (uuid) | yes | The project the repository belongs to. | | `data.repository_id` | string (uuid) | yes | The repository's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE2", "type": "repository.disconnected", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "full_name": "zuse-technologies/legacy-importer", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "repository_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # repository.reconnected Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-reconnected A disconnected repository is brought back. ### When this fires A person reconnects the repository from the Repositories page, or a token calls the reconnect endpoint. It is back in the live listing. ### Related events - `repository.disconnected`: the state it came back from. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `repository.reconnected` | yes | The event name, always `repository.reconnected` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.full_name` | string | yes | The owner and the name joined. | | `data.project_id` | string (uuid) | yes | The project the repository belongs to. | | `data.repository_id` | string (uuid) | yes | The repository's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE3", "type": "repository.reconnected", "created_at": "2026-11-06T09:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "full_name": "zuse-technologies/legacy-importer", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "repository_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # repository.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/repository/repository-updated A repository's fields change. ### When this fires Any of the repository's own columns change: the provider, the owner, the name, the full name, the address, the default branch, whether it is private or the component it builds. Disconnecting raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after. ### Related events - `repository.disconnected`: setting a repository aside, which is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `repository.updated` | yes | The event name, always `repository.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.full_name` | string | yes | The full name after the change. | | `data.project_id` | string (uuid) | yes | The project the repository belongs to. | | `data.repository_id` | string (uuid) | yes | The repository's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE1", "type": "repository.updated", "created_at": "2026-10-27T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "default_branch": { "from": "main", "to": "release/2026.11" } }, "full_name": "zuse-technologies/customer-portal-web", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "repository_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # requirement.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement The lifecycle of a project's requirements: a functional or non-functional requirement added, changed, moved along its review lifecycle, put back to an earlier version and deleted. A mirror of the specification, a test management tool that derives cases from the requirements and the people's notifications subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the reference and what the move itself carries, never a snapshot of the requirement. Read the [requirements endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/requirements) for the record and its versions. ## Required ability Tokens subscribing to `requirement.*` events need `requirement:view` at mint time. ## Events - [`requirement.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-created) — A requirement is added. - [`requirement.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-updated) — A requirement's fields change. - [`requirement.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-status-changed) — A requirement moves along its review lifecycle. - [`requirement.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-restored) — An earlier version of a requirement is put back. - [`requirement.quality_reviewed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-quality-reviewed) — A requirement's quality is reviewed. - [`requirement.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-deleted) — A requirement is deleted. --- # requirement.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-created A requirement is added. ### When this fires A person adds a requirement on the Requirements page, an agent drafts one, a brief import is accepted, or a token creates one through the API. The payload carries the reference the requirement was numbered with, its kind, the status it starts in and its priority. ### Related events - `requirement.updated`: the next change. - `requirement.status_changed`: the requirement's review lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `requirement.created` | yes | The event name, always `requirement.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.priority` | `must`, `should`, `could`, `wont` | yes | Its MoSCoW priority. | | `data.project_id` | string (uuid) | yes | The project the requirement belongs to. | | `data.reference` | string | yes | The key it was numbered with, such as `CRM-FR-12` or `CRM-NFR-3`. | | `data.requirement_id` | string (uuid) | yes | The requirement's id. | | `data.status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | yes | Where it starts: `draft` unless the creator said otherwise. | | `data.title` | string | yes | What the requirement is called. | | `data.type` | `functional`, `non_functional` | yes | The kind. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE0", "type": "requirement.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "priority": "must", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-NFR-3", "requirement_id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "status": "draft", "title": "Tier page answers within a third of a second", "type": "non_functional" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # requirement.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-deleted A requirement is deleted. ### When this fires A person deletes the requirement from the Requirements page, or a token calls the delete endpoint. The row and its versions are gone; the payload carries what a mirror needs to drop its own copy. ### Related events - `requirement.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `requirement.deleted` | yes | The event name, always `requirement.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project the requirement belonged to. | | `data.reference` | string | yes | The requirement's key, never reused. | | `data.requirement_id` | string (uuid) | yes | The requirement's id. | | `data.title` | string | yes | What the requirement was called. | | `data.type` | `functional`, `non_functional` | yes | The kind. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE4", "type": "requirement.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-NFR-3", "requirement_id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "title": "Tier page answers within a third of a second", "type": "non_functional" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # requirement.quality_reviewed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-quality-reviewed A requirement's quality is reviewed. ### When this fires A person presses Review now on the requirement's flyout, an agent calls `review_quality`, or a token posts to the requirement's quality reviews endpoint. The payload carries the score, how many findings the review holds and the version of the requirement that was read; read the [requirements endpoint](/api/v1/reference/requirements) for the findings themselves. ### Related events - `requirement.updated`: the requirement changes once the findings are addressed. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `requirement.quality_reviewed` | yes | The event name, always `requirement.quality_reviewed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.findings_count` | integer | yes | How many findings the review holds; 0 when the record was found sound. | | `data.project_id` | string (uuid) | yes | The project the requirement belongs to. | | `data.reference` | string | yes | The requirement's reference. | | `data.requirement_id` | string (uuid) | yes | The requirement's id. | | `data.review_id` | string (uuid) | yes | The review's id; read the endpoint for its findings. | | `data.score` | integer | yes | The score, 0 to 100. | | `data.version` | integer \| null | yes | The version of the requirement that was read; null when it had no version yet. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ1", "type": "requirement.quality_reviewed", "created_at": "2026-10-28T11:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "findings_count": 2, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-FR-12", "requirement_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "review_id": "b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d01", "score": 71, "version": 3 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # requirement.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-restored An earlier version of a requirement is put back. ### When this fires A person restores a version from the requirement's history, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, and the payload carries both numbers. ### Related events - `requirement.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `requirement.restored` | yes | The event name, always `requirement.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project the requirement belongs to. | | `data.reference` | string | yes | The requirement's key. | | `data.requirement_id` | string (uuid) | yes | The requirement's id. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.title` | string | yes | What the requirement is called after the restore. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE3", "type": "requirement.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-NFR-3", "requirement_id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "restored_from_version": 2, "title": "Tier page answers within a third of a second", "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # requirement.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-status-changed A requirement moves along its review lifecycle. ### When this fires A person moves the requirement from the Requirements page or the flyout, or a token calls the status endpoint. The payload carries the status before and after, so a mirror that shows the client only agreed requirements reacts without diffing a column. ### Related events - `requirement.deleted`: the end of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `requirement.status_changed` | yes | The event name, always `requirement.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.previous_status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | yes | Where the requirement was. | | `data.project_id` | string (uuid) | yes | The project the requirement belongs to. | | `data.reference` | string | yes | The requirement's key. | | `data.requirement_id` | string (uuid) | yes | The requirement's id. | | `data.status` | `draft`, `in_review`, `approved`, `baselined`, `deprecated` | yes | Where the requirement is now. | | `data.title` | string | yes | What the requirement is called. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE2", "type": "requirement.status_changed", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "previous_status": "in_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-NFR-3", "requirement_id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "status": "approved", "title": "Tier page answers within a third of a second" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # requirement.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/requirement/requirement-updated A requirement's fields change. ### When this fires Any of the requirement's own columns change: the title, the statement, the rationale, the category, the source, the priority, the measure, the verification method or the owner. A status move raises its own event, not this one. `changes` holds one entry per column that changed, each with the value before and after. ### Related events - `requirement.status_changed`: the move that is deliberately not an update. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `requirement.updated` | yes | The event name, always `requirement.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.project_id` | string (uuid) | yes | The project the requirement belongs to. | | `data.reference` | string | yes | The requirement's key. | | `data.requirement_id` | string (uuid) | yes | The requirement's id. | | `data.title` | string | yes | What the requirement is called after the change. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VE1", "type": "requirement.updated", "created_at": "2026-10-28T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "priority": { "from": "should", "to": "must" }, "measure": { "from": null, "to": "p95 under 300 ms at 500 concurrent users, measured on staging" } }, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-NFR-3", "requirement_id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "title": "Tier page answers within a third of a second" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # research.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research The life of a research run: Heimdall asked to read a project's domain, done with notes and suggestions, or failed. A dashboard that follows how a project's context fills up and a bot that nudges the analyst when the notes land subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads carry ids and counts, never the notes: read the [research endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/research) for those. The suggestions a run leaves are kept or dismissed like a brief import's, with the `import.suggestion_*` events. ## Required ability Tokens subscribing to `research.*` events need `import:view` at mint time. ## Events - [`research.started`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-started) — A person asks Heimdall to research the project. - [`research.ready`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-ready) — Heimdall finished the research. - [`research.failed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-failed) — The run failed. --- # research.failed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-failed The run failed. ### When this fires The queue could not finish the run: the model did not answer, its answer held nothing that could be read, the team's monthly AI budget had no room, or the person who asked is no longer a member. The run is left failed with the reason, and a person asks again. ### Related events - `research.started`: a person asks again. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `research.failed` | yes | The event name, always `research.failed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project researched. | | `data.reason` | string | yes | Why it failed, as a person reads it. | | `data.run_id` | string (uuid) | yes | The run's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VN3", "type": "research.failed", "created_at": "2026-10-28T14:03:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reason": "Heimdall could not reach its model just now. Nothing was changed; try again in a moment.", "run_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a60" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # research.ready Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-ready Heimdall finished the research. ### When this fires The queue finished the run, wrote its notes and left its suggestions on the project. The payload carries the counts; read the run for the notes and the project's suggestions for what waits. ### Related events - `import.suggestion_accepted`: a person keeps one of the suggestions. - `import.suggestion_rejected`: a person dismisses one. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `research.ready` | yes | The event name, always `research.ready` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.note_count` | integer | yes | How many notes the run wrote. | | `data.project_id` | string (uuid) | yes | The project researched. | | `data.run_id` | string (uuid) | yes | The run's id. | | `data.suggestion_count` | integer | yes | How many suggestions it left, after dismissed and held ideas were left out. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VN2", "type": "research.ready", "created_at": "2026-10-28T14:03:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "note_count": 2, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "run_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a60", "suggestion_count": 3 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # research.started Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/research/research-started A person asks Heimdall to research the project. ### When this fires A person presses Research the domain on the dashboard rail, or a token posts to the project's research endpoint. The run is queued; the work follows on the queue as the person who asked. ### Related events - `research.ready`: the run is done. - `research.failed`: the run failed. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `research.started` | yes | The event name, always `research.started` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project researched. | | `data.run_id` | string (uuid) | yes | The run's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VN1", "type": "research.started", "created_at": "2026-10-28T14:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "run_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a60" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # role.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role Role creation, edits and deletion. A role is the permission set one member holds on a team; every team starts with the eight default roles and admins add their own. These are account-level events: `project_id` is always `null`. ## Example envelope ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V80", "type": "role.created", "created_at": "2026-11-01T09:00:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "code": "support_agent", "name": "Support Agent", "permissions": ["ticket:view-any", "ticket:view", "ticket:respond"], "role_id": "d05e1a83-7c46-4f29-b613-8ae407c9d251", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86" } } ``` ## Required ability Tokens subscribing to `role.*` events need `role:view` at mint time. ## Events - [`role.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-created) — A role is created. - [`role.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-updated) — A role's name, description or permissions change. - [`role.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-deleted) — A custom role is deleted. --- # role.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-created A role is created. ### When this fires Someone with `role:create` creates a role, in **Settings → Roles** or through `POST /v1/roles`. It also fires eight times when a team is created, once per default role, in the team's own transaction. ### Related events - `role.updated`: subsequent edits. - `role.deleted`: terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `role.created` | yes | The event name, always `role.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.code` | string | yes | The stable code permissions are addressed by, unique within the team. | | `data.name` | string | yes | The display name. | | `data.permissions` | array of string | yes | The permission codes the role grants, in `entity:action` form; may be empty. | | `data.role_id` | string (uuid) | yes | The role's id. | | `data.team_id` | string (uuid) | yes | The team the role belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V81", "type": "role.created", "created_at": "2026-11-01T09:00:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "code": "support_agent", "name": "Support Agent", "permissions": [ "ticket:view-any", "ticket:view", "ticket:respond" ], "role_id": "d05e1a83-7c46-4f29-b613-8ae407c9d251", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # role.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-deleted A custom role is deleted. ### When this fires Whoever created the role, or the team owner, deletes it. A default role cannot be deleted. The role's permissions are detached on the way out; its holders stay in the team with nothing beyond what their groups grant until they are given another role. ### Related events - `role.created`: first event in the lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `role.deleted` | yes | The event name, always `role.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.code` | string | yes | The role's code. | | `data.name` | string | yes | The role's last name. | | `data.role_id` | string (uuid) | yes | The role that was deleted. | | `data.team_id` | string (uuid) | yes | The team. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V83", "type": "role.deleted", "created_at": "2027-01-08T16:00:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "code": "support_agent", "name": "Senior Support Agent", "role_id": "d05e1a83-7c46-4f29-b613-8ae407c9d251", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # role.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/role/role-updated A role's name, description or permissions change. ### When this fires Someone with `role:update` edits the role. The permission set is replaced as a whole when it is sent, so one edit may add and remove several codes; `changes` says which columns moved. ### Related events - `team.member_role_changed`: a member moving between roles, which is not a change to the role. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `role.updated` | yes | The event name, always `role.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each mapped to an object with `from` and `to`: `name`, `description` or `permissions`. | | `data.code` | string | yes | The role's code, which never changes. | | `data.name` | string | yes | The display name after the change. | | `data.role_id` | string (uuid) | yes | The role. | | `data.team_id` | string (uuid) | yes | The team. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V82", "type": "role.updated", "created_at": "2026-11-10T11:20:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "changes": { "name": { "from": "Support Agent", "to": "Senior Support Agent" }, "permissions": { "from": [ "ticket:view-any", "ticket:view" ], "to": [ "ticket:view-any", "ticket:view", "ticket:respond" ] } }, "code": "support_agent", "name": "Senior Support Agent", "role_id": "d05e1a83-7c46-4f29-b613-8ae407c9d251", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # stakeholder.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder The lifecycle of a project's stakeholders: who was added to the record, what changed about them and who was removed. A stakeholder is a person the project answers to, so a mirror of the project record subscribes to this family beside `project.*` to keep its own view of who decides what. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the name and what the transition itself carries, never a snapshot of the stakeholder and never their contact details. Read the [stakeholders endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/stakeholders) for the record. ## Required ability Tokens subscribing to `stakeholder.*` events need `stakeholder:view` at mint time. ## Events - [`stakeholder.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-created) — A stakeholder is added. - [`stakeholder.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-updated) — A stakeholder's fields change. - [`stakeholder.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-deleted) — A stakeholder is removed. - [`stakeholder.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-restored) — An earlier version of a stakeholder is put back. --- # stakeholder.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-created A stakeholder is added. ### When this fires A person adds a stakeholder on the Stakeholders page or in the intake wizard, or a token adds one through the API. The payload carries the id a mirror needs to open the stakeholder, the side they speak for and the role they hold. ### Related events - `stakeholder.updated`: the next change. - `project.member_added`: the account a client stakeholder signs in with, when they are invited as a guest. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `stakeholder.created` | yes | The event name, always `stakeholder.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The stakeholder's name. | | `data.project_id` | string (uuid) | yes | The project the stakeholder belongs to. | | `data.role` | `sponsor`, `product_owner`, `business_owner`, `subject_matter_expert`, `end_user_representative`, `technical_lead`, `architect`, `security`, `legal`, `finance`, `operations`, `other` | yes | What they do for the project. | | `data.side` | `client`, `zuse`, `third_party` | yes | Whose side they speak for. | | `data.stakeholder_id` | string (uuid) | yes | The stakeholder's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V90", "type": "stakeholder.created", "created_at": "2026-10-20T09:10:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "name": "Jane Perera", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "role": "product_owner", "side": "client", "stakeholder_id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # stakeholder.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-deleted A stakeholder is removed. ### When this fires A person removes the stakeholder from the Stakeholders page, or a token calls the delete endpoint. The row is gone; the payload carries what a mirror needs to drop its own. ### Related events - `stakeholder.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `stakeholder.deleted` | yes | The event name, always `stakeholder.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The stakeholder's name. | | `data.project_id` | string (uuid) | yes | The project the stakeholder belonged to. | | `data.stakeholder_id` | string (uuid) | yes | The stakeholder's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V92", "type": "stakeholder.deleted", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "name": "Jane Perera", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "stakeholder_id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # stakeholder.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-restored An earlier version of a stakeholder is put back. ### When this fires A person restores a version from the stakeholder's history on the Stakeholders tab, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, and the payload carries both numbers. ### Related events - `stakeholder.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `stakeholder.restored` | yes | The event name, always `stakeholder.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The stakeholder's name after the restore. | | `data.project_id` | string (uuid) | yes | The project the stakeholder belongs to. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.stakeholder_id` | string (uuid) | yes | The stakeholder's id. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VKC", "type": "stakeholder.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "name": "Jane Perera", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "restored_from_version": 2, "stakeholder_id": "d3e4f5a6-b7c8-4d9e-8f0a-1b2c3d4e5f70", "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # stakeholder.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/stakeholder/stakeholder-updated A stakeholder's fields change. ### When this fires Any of the stakeholder's own columns change: their name, contact details, organisation, side, title, role, influence, interest, RACI default, approver flags or linked account. `changes` holds one entry per column that changed, each with the value before and after. ### Related events - `stakeholder.deleted`: the end of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `stakeholder.updated` | yes | The event name, always `stakeholder.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.name` | string | yes | The stakeholder's name after the change. | | `data.project_id` | string (uuid) | yes | The project the stakeholder belongs to. | | `data.stakeholder_id` | string (uuid) | yes | The stakeholder's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V91", "type": "stakeholder.updated", "created_at": "2026-10-27T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "approves_releases": { "from": false, "to": true }, "raci_default": { "from": null, "to": "accountable" } }, "name": "Jane Perera", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "stakeholder_id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c01" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # team.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team Team creation, rename and deletion, and the comings and goings of the people in it. A team is the tenant every token is scoped to, so these are account-level events: `project_id` is always `null` and an endpoint confined to a project never receives them. ## Example envelope ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V70", "type": "team.member_joined", "created_at": "2026-11-03T14:22:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "role": "developer", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80" } } ``` `data` is flat and deliberately thin: ids, role codes, plus whatever the transition itself carries. It is not a snapshot of the team or the person. Read [the team members endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/team-members) for the profile, which keeps a member's email behind the `team-member:view` ability. The one exception is `team.member_invited`, which carries the invitee's `email`: an invitee has no user record to look up yet, so the id-only rule cannot apply to it. ## Required ability Tokens subscribing to `team.*` events need `team-member:view-any` at mint time, the ability that lists a team's roster, for all seven events. ## Events - [`team.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-created) — A team is created. - [`team.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-updated) — A team is renamed. - [`team.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-deleted) — A team with no projects is deleted. - [`team.member_invited`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-invited) — An invitation is sent to a staff member or a client guest. - [`team.member_joined`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-joined) — An invitee accepts and becomes a member. - [`team.member_removed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-removed) — A member is removed from the team. - [`team.member_role_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-role-changed) — A member moves to another of the team's roles. --- # team.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-created A team is created. ### When this fires An admin creates a team under **Settings → Teams**, or a token creates one through the API. The creator becomes the owner and the eight default roles are seeded in the same transaction. > **Delivered to the creator's other teams' endpoints.** Endpoints are scoped to a team, and an endpoint cannot exist for a team before that team does, so the new team's own endpoints never see this event; endpoints registered under the creator's existing teams do not either, because an event is fanned out within the team it happened in. In practice this event reaches no subscriber today; poll [the teams list](/api/v1/reference/teams) to observe team creation. ### Related events - `role.created`: eight of them follow at once, one per seeded role. - `team.member_invited`: typical follow-up. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `team.created` | yes | The event name, always `team.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The team's name. | | `data.team_id` | string (uuid) | yes | The team's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V71", "type": "team.created", "created_at": "2026-11-01T09:00:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "name": "Zuse Technologies", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # team.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-deleted A team with no projects is deleted. ### When this fires The owner deletes the team. A team that still holds projects cannot be deleted, so the registry has to be emptied first, and an owner cannot delete their only team; both refusals are `422` on the endpoint and a message in the app. > **Not delivered to the deleted team's endpoints.** An endpoint is scoped to a team, and deleting the team removes its endpoints in the same operation, so this event reaches no subscriber today. ### Related events - `team.created`: first event in the lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `team.deleted` | yes | The event name, always `team.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The team's last name. | | `data.team_id` | string (uuid) | yes | The team that was deleted. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V73", "type": "team.deleted", "created_at": "2027-02-01T10:05:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "name": "Pilot Team", "team_id": "b94f1e62-5da3-4c8f-9726-3fe0f81b4d97" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # team.member_invited Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-invited An invitation is sent to a staff member or a client guest. ### When this fires Someone with `team-member:invite` invites a person by email, as a staff role or as a `guest` onto one project. The invitee becomes a member only once they accept and `team.member_joined` fires; withdrawing a pending invitation raises nothing. ### Related events - `team.member_joined`: fires on acceptance. - `project.member_added`: fires alongside the join for a guest, naming the project. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `team.member_invited` | yes | The event name, always `team.member_invited` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.email` | string (email) | yes | The address the invitation was sent to. | | `data.invitation_id` | string (uuid) | yes | The invitation, the id the withdraw endpoint takes. | | `data.role` | string | yes | The role code the invitee will hold on acceptance, such as `developer` or `guest`. | | `data.team_id` | string (uuid) | yes | The team the invitation is for. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V74", "type": "team.member_invited", "created_at": "2026-11-02T15:30:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "email": "priya@example.com", "invitation_id": "d05e1a83-7c46-4f29-b613-8ae407c9d251", "role": "developer", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # team.member_joined Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-joined An invitee accepts and becomes a member. ### When this fires The invitee follows the link in their invitation, creates their account or signs in, and the membership row is written with the role the invitation named. ### Related events - `team.member_invited`: predecessor. - `team.member_role_changed`: typical follow-up over time. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `team.member_joined` | yes | The event name, always `team.member_joined` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.role` | string | yes | The role code the member holds, as invited. | | `data.team_id` | string (uuid) | yes | The team joined. | | `data.user_id` | string (uuid) | yes | The new member's user id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V75", "type": "team.member_joined", "created_at": "2026-11-03T14:22:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "role": "developer", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # team.member_removed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-removed A member is removed from the team. ### When this fires Someone with `team-member:remove` removes a member, in the app or through the API. The owner cannot be removed. The person's project membership rows go with them, each raising `project.member_removed`. ### Related events - `team.member_joined`: predecessor. - `project.member_removed`: one per project row the person held. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `team.member_removed` | yes | The event name, always `team.member_removed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.team_id` | string (uuid) | yes | The team the member was on. | | `data.user_id` | string (uuid) | yes | The removed member's user id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V76", "type": "team.member_removed", "created_at": "2027-01-15T10:05:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # team.member_role_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-member-role-changed A member moves to another of the team's roles. ### When this fires Someone with `team-member:update-role` moves a member onto another role, through `PATCH /v1/teams/{team}/members/{member}/role` or the roster page. The owner has no role to change. ### Related events - `role.updated`: emits when the role's own permissions change rather than who holds it. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `team.member_role_changed` | yes | The event name, always `team.member_role_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.previous_role` | string | yes | The role code the member held before. | | `data.role` | string | yes | The role code the member holds now. | | `data.team_id` | string (uuid) | yes | The team. | | `data.user_id` | string (uuid) | yes | The member. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V77", "type": "team.member_role_changed", "created_at": "2026-12-01T09:45:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "previous_role": "developer", "role": "project_manager", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # team.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/team/team-updated A team is renamed. ### When this fires The team's name changes, through **Settings → Teams** or `PATCH /v1/teams/{team}`. That is the whole of it: `name` is the only mutable field on a team, and ownership does not transfer. ### Related events - `team.created`: predecessor. - `team.deleted`: terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `team.updated` | yes | The event name, always `team.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each mapped to an object with `from` and `to`; `name` today. | | `data.name` | string | yes | The new name. | | `data.team_id` | string (uuid) | yes | The team. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V72", "type": "team.updated", "created_at": "2026-11-05T10:05:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "changes": { "name": { "from": "Zuse Technologies", "to": "Zuse Technologies (Pvt) Ltd" } }, "name": "Zuse Technologies (Pvt) Ltd", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # token.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token Personal access token minting and revocation. A token is what authorises every REST and MCP call, so a security mirror subscribes here to watch credential churn on the team. These are account-level events: `project_id` is `null` unless the token was confined to one project, in which case it names that project. ## Example envelope ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V90", "type": "token.created", "created_at": "2026-11-04T08:30:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "abilities": ["project:view-any", "project:view", "activity:read"], "expires_at": null, "name": "Nightly report", "token_id": "1902", "user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80" } } ``` The plaintext token is never carried by an event. `token_id` is the token's integer id serialised as a string, the one identifier in the platform that is not a UUID. ## Required ability Tokens subscribing to `token.*` events need `token:view` at mint time. ## Events - [`token.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token/token-created) — A token is minted. - [`token.revoked`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token/token-revoked) — A token is revoked. --- # token.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token/token-created A token is minted. ### When this fires A person mints a token under **Account Settings → API Tokens**, or a token mints one through the API within the abilities it holds itself. The payload names the abilities granted, scopes excluded, so a mirror can tell a read-only reporting token from one that can delete. ### Related events - `token.revoked`: terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `token.created` | yes | The event name, always `token.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.abilities` | array of string | yes | The gate abilities the token holds, as the ability catalogue lists them; scopes excluded. | | `data.expires_at` | string \| null (date-time) | yes | When the token stops working; null for a token that does not expire, which is every token today. | | `data.name` | string | yes | The label the token was minted with. | | `data.token_id` | string | yes | The token's integer id, serialised as a string. | | `data.user_id` | string (uuid) | yes | The person the token belongs to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V91", "type": "token.created", "created_at": "2026-11-04T08:30:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "abilities": [ "project:view-any", "project:view", "activity:read" ], "expires_at": null, "name": "Nightly report", "token_id": "1902", "user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # token.revoked Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/token/token-revoked A token is revoked. ### When this fires The holder revokes the token on the tokens page, or through `DELETE /v1/tokens/{token}` with another token of theirs. Revocation is immediate: the next request with it answers `401`. ### Related events - `token.created`: predecessor. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `token.revoked` | yes | The event name, always `token.revoked` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.name` | string | yes | The label the token was minted with. | | `data.token_id` | string | yes | The token's integer id, serialised as a string. | | `data.user_id` | string (uuid) | yes | The person the token belonged to. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3V92", "type": "token.revoked", "created_at": "2027-01-20T17:05:00+00:00", "api_version": "2026-11-01", "project_id": null, "data": { "name": "Nightly report", "token_id": "1902", "user_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # use_case.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case The lifecycle of a project's use cases: a use case added, changed, moved along its review lifecycle, given new steps, put back to an earlier version and deleted. A mirror of the specification, a test management tool that derives cases from the flows and the people's notifications subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the reference and what the move itself carries, never a snapshot of the use case. Read the [use cases endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/use-cases) for the record, its steps and its versions. ## Required ability Tokens subscribing to `use_case.*` events need `use-case:view` at mint time. ## Events - [`use_case.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-created) — A use case is added. - [`use_case.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-updated) — A use case's fields change. - [`use_case.status_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-status-changed) — A use case moves along its review lifecycle. - [`use_case.steps_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-steps-changed) — A use case's steps are replaced. - [`use_case.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-restored) — An earlier version of a use case is put back. - [`use_case.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-deleted) — A use case is deleted. --- # use_case.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-created A use case is added. ### When this fires A person adds a use case on the Use Cases page, a brief import is accepted, or a token creates one through the API. The payload carries the reference the use case was numbered with, the status it starts in and its priority. ### Related events - `use_case.updated`: the next change. - `use_case.status_changed`: the use case's review lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `use_case.created` | yes | The event name, always `use_case.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.priority` | `must`, `should`, `could`, `wont` | yes | Its MoSCoW priority. | | `data.project_id` | string (uuid) | yes | The project the use case belongs to. | | `data.reference` | string | yes | The key it was numbered with, such as `CRM-UC-4`. | | `data.status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where it starts: `draft` unless the creator said otherwise. | | `data.title` | string | yes | What the use case is called. | | `data.use_case_id` | string (uuid) | yes | The use case's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VG0", "type": "use_case.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "priority": "must", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-UC-4", "status": "draft", "title": "Redeem a voucher at checkout", "use_case_id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # use_case.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-deleted A use case is deleted. ### When this fires A person deletes the use case from the Use Cases page, or a token calls the delete endpoint. The row, its steps and its versions are gone; the payload carries what a mirror needs to drop its own copy. ### Related events - `use_case.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `use_case.deleted` | yes | The event name, always `use_case.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project the use case belonged to. | | `data.reference` | string | yes | The use case's key, never reused. | | `data.title` | string | yes | What the use case was called. | | `data.use_case_id` | string (uuid) | yes | The use case's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VG5", "type": "use_case.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-UC-4", "title": "Redeem a voucher at checkout", "use_case_id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # use_case.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-restored An earlier version of a use case is put back. ### When this fires A person restores a version from the use case's history, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, the steps included, and the payload carries both numbers. ### Related events - `use_case.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `use_case.restored` | yes | The event name, always `use_case.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project the use case belongs to. | | `data.reference` | string | yes | The use case's key. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.title` | string | yes | What the use case is called after the restore. | | `data.use_case_id` | string (uuid) | yes | The use case's id. | | `data.version` | integer | yes | The number of the version the restore wrote. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VG4", "type": "use_case.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-UC-4", "restored_from_version": 2, "title": "Redeem a voucher at checkout", "use_case_id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091", "version": 5 } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # use_case.status_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-status-changed A use case moves along its review lifecycle. ### When this fires A person moves the use case from the Use Cases page or the flyout, or a token calls the status endpoint. The payload carries the status before and after, so a mirror that shows the client only agreed use cases reacts without diffing a column. ### Related events - `use_case.deleted`: the end of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `use_case.status_changed` | yes | The event name, always `use_case.status_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.previous_status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the use case was. | | `data.project_id` | string (uuid) | yes | The project the use case belongs to. | | `data.reference` | string | yes | The use case's key. | | `data.status` | `draft`, `in_review`, `approved`, `deprecated` | yes | Where the use case is now. | | `data.title` | string | yes | What the use case is called. | | `data.use_case_id` | string (uuid) | yes | The use case's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VG2", "type": "use_case.status_changed", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "previous_status": "in_review", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-UC-4", "status": "approved", "title": "Redeem a voucher at checkout", "use_case_id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # use_case.steps_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-steps-changed A use case's steps are replaced. ### When this fires A token calls the steps endpoint with the full list. The payload carries how many rows were added, changed and removed; a replace that changes nothing emits nothing. Steps saved together with the use case's fields travel inside the use case's version and raise `use_case.updated` for the fields. ### Related events - `use_case.updated`: the fields themselves. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `use_case.steps_changed` | yes | The event name, always `use_case.steps_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.added` | integer | yes | How many rows were added. | | `data.changed` | integer | yes | How many rows were changed. | | `data.project_id` | string (uuid) | yes | The project the use case belongs to. | | `data.reference` | string | yes | The use case's key. | | `data.removed` | integer | yes | How many rows were removed. | | `data.use_case_id` | string (uuid) | yes | The use case's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VG3", "type": "use_case.steps_changed", "created_at": "2026-11-03T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "added": 1, "changed": 1, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-UC-4", "removed": 1, "use_case_id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # use_case.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/use-case/use-case-updated A use case's fields change. ### When this fires Any of the use case's own columns change: the title, the actor and the secondary actors, the goal, the trigger, the conditions, the frequency or the priority. A status move and a replace of the steps raise their own events, not this one. `changes` holds one entry per column that changed, each with the value before and after. Steps saved together with the fields travel inside the use case's version. ### Related events - `use_case.steps_changed`, `use_case.status_changed`: the changes that are deliberately not updates. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `use_case.updated` | yes | The event name, always `use_case.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.project_id` | string (uuid) | yes | The project the use case belongs to. | | `data.reference` | string | yes | The use case's key. | | `data.title` | string | yes | What the use case is called after the change. | | `data.use_case_id` | string (uuid) | yes | The use case's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VG1", "type": "use_case.updated", "created_at": "2026-10-28T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "priority": { "from": "should", "to": "must" }, "frequency": { "from": null, "to": "A few hundred times a day in a campaign week" } }, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-UC-4", "title": "Redeem a voucher at checkout", "use_case_id": "e6f7a8b9-c0d1-4e2f-9a3b-4c5d6e7f8091" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # work_item.* events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item The lifecycle of a project's work items: a story, task, sub-task, bug, spike or improvement added, changed, moved along its lifecycle, handed to a person, given new acceptance criteria, put back to an earlier version and deleted. The board, a mirror of the tracker, the Git integration and the people's notifications subscribe to this family. Every event in the family is scoped to the project, so `project_id` in the envelope is always set and an endpoint confined to one project receives only that project's events. The payloads are thin on purpose: the ids, the reference and what the transition itself carries, never a snapshot of the item. Read the [work items endpoint](https://alm-docs.labs.zuselab.dev/api/v1/reference/work-items) for the record, its criteria and its versions. ## Required ability Tokens subscribing to `work_item.*` events need `work-item:view` at mint time. ## Events - [`work_item.created`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-created) — An item is added. - [`work_item.updated`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-updated) — An item's fields change. - [`work_item.transitioned`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-transitioned) — An item moves along its lifecycle. - [`work_item.assigned`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-assigned) — An item is handed to a person, or to nobody. - [`work_item.criteria_changed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-criteria-changed) — An item's acceptance criteria are replaced. - [`work_item.restored`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-restored) — An earlier version of an item is put back. - [`work_item.quality_reviewed`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-quality-reviewed) — An item's quality is reviewed. - [`work_item.deleted`](https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-deleted) — An item is deleted. --- # work_item.assigned Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-assigned An item is handed to a person, or to nobody. ### When this fires A person assigns the item from the Backlog page, an agent calls `assign_work_item`, or a token calls the assign endpoint. The payload carries the person before and after, so their notifications and the board react; a hand-over to the person who already holds it emits nothing. ### Related events - `work_item.transitioned`: where the item stands. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `work_item.assigned` | yes | The event name, always `work_item.assigned` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.assignee_id` | string \| null (uuid) | yes | Who holds the item now; null for nobody. | | `data.previous_assignee_id` | string \| null (uuid) | yes | Who held it before; null for nobody. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.reference` | string | yes | The item's key. | | `data.title` | string | yes | What the item is called. | | `data.work_item_id` | string (uuid) | yes | The item's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD3", "type": "work_item.assigned", "created_at": "2026-11-02T09:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "assignee_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f", "previous_assignee_id": null, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-142", "title": "Tier upgrade email", "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # work_item.created Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-created An item is added. ### When this fires A person adds an item on the Backlog page, an agent drafts a story, a brief import is accepted, or a token creates one through the API. The payload carries the reference the item was numbered with, its kind, the status it starts in, its priority and the epic it hangs under. ### Related events - `work_item.updated`: the next change. - `work_item.transitioned`: the item's lifecycle. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `work_item.created` | yes | The event name, always `work_item.created` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.epic_id` | string \| null (uuid) | yes | The epic the item hangs under; null when none. | | `data.priority` | `lowest`, `low`, `medium`, `high`, `highest` | yes | How urgent it is. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.reference` | string | yes | The key the item was numbered with, such as `CRM-142`. | | `data.status` | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` | yes | Where the item starts: `backlog` unless the creator said otherwise. | | `data.title` | string | yes | What the item is called. | | `data.type` | `story`, `task`, `subtask`, `bug`, `spike`, `improvement` | yes | The kind. | | `data.work_item_id` | string (uuid) | yes | The item's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD0", "type": "work_item.created", "created_at": "2026-10-28T09:20:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "epic_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "priority": "high", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-142", "status": "backlog", "title": "Tier upgrade email", "type": "story", "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # work_item.criteria_changed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-criteria-changed An item's acceptance criteria are replaced. ### When this fires A person saves the criteria rows alone, or a token calls the criteria endpoint with the full list. The payload carries how many rows were added, changed and removed; a replace that changes nothing emits nothing. Criteria saved together with the item's fields travel inside the item's version and raise `work_item.updated` for the fields. ### Related events - `work_item.updated`: the fields themselves. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `work_item.criteria_changed` | yes | The event name, always `work_item.criteria_changed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.added` | integer | yes | How many rows were added. | | `data.changed` | integer | yes | How many rows were changed. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.reference` | string | yes | The item's key. | | `data.removed` | integer | yes | How many rows were removed. | | `data.work_item_id` | string (uuid) | yes | The item's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD4", "type": "work_item.criteria_changed", "created_at": "2026-11-03T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "added": 2, "changed": 1, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-142", "removed": 0, "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # work_item.deleted Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-deleted An item is deleted. ### When this fires A person deletes the item from the Backlog page, or a token calls the delete endpoint. The row, its criteria and its versions are gone and the items under it have lost their parent; the payload carries what a mirror needs to drop its own copy. ### Related events - `work_item.created`: the beginning of the row. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `work_item.deleted` | yes | The event name, always `work_item.deleted` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project the item belonged to. | | `data.reference` | string | yes | The item's key, never reused. | | `data.title` | string | yes | What the item was called. | | `data.type` | `story`, `task`, `subtask`, `bug`, `spike`, `improvement` | yes | The kind. | | `data.work_item_id` | string (uuid) | yes | The item's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD6", "type": "work_item.deleted", "created_at": "2026-11-07T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-142", "title": "Tier upgrade email", "type": "story", "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # work_item.quality_reviewed Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-quality-reviewed An item's quality is reviewed. ### When this fires A person presses Review now on the item's flyout, an agent calls `review_quality`, or a token posts to the item's quality reviews endpoint. The payload carries the score, how many findings the review holds and the version of the item that was read; read the [work items endpoint](/api/v1/reference/work-items) for the findings themselves. ### Related events - `work_item.updated`: the item changes once the findings are addressed. - `work_item.criteria_changed`: the criteria a finding named are rewritten. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `work_item.quality_reviewed` | yes | The event name, always `work_item.quality_reviewed` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.findings_count` | integer | yes | How many findings the review holds; 0 when the record was found sound. | | `data.project_id` | string (uuid) | yes | The project the work item belongs to. | | `data.reference` | string | yes | The work item's reference. | | `data.review_id` | string (uuid) | yes | The review's id; read the endpoint for its findings. | | `data.score` | integer | yes | The score, 0 to 100. | | `data.version` | integer \| null | yes | The version of the work item that was read; null when it had no version yet. | | `data.work_item_id` | string (uuid) | yes | The item's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VQ1", "type": "work_item.quality_reviewed", "created_at": "2026-10-28T11:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "findings_count": 2, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-142", "review_id": "b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d01", "score": 71, "version": 3, "work_item_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # work_item.restored Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-restored An earlier version of an item is put back. ### When this fires A person restores a version from the item's history, or a token calls the restore endpoint or the `restore_revision` tool. The restore writes a new version that points at the one put back, the criteria included, and the payload carries both numbers. ### Related events - `work_item.updated`: an ordinary change to the record. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `work_item.restored` | yes | The event name, always `work_item.restored` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.reference` | string | yes | The item's key. | | `data.restored_from_version` | integer | yes | The number of the version that was put back. | | `data.title` | string | yes | What the item is called after the restore. | | `data.version` | integer | yes | The number of the version the restore wrote. | | `data.work_item_id` | string (uuid) | yes | The item's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD5", "type": "work_item.restored", "created_at": "2026-11-06T10:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-142", "restored_from_version": 2, "title": "Tier upgrade email", "version": 5, "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # work_item.transitioned Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-transitioned An item moves along its lifecycle. ### When this fires A person moves the item from the Backlog page or the board, an agent calls `move_work_item`, or a token calls the status endpoint. The payload carries the status before and after and, for a close, the resolution, so the board moves the card and a mirror knows why an item was dropped. ### Related events - `work_item.assigned`: who holds the item. - `work_item.deleted`: the terminal state. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `work_item.transitioned` | yes | The event name, always `work_item.transitioned` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.previous_status` | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` | yes | Where the item was. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.reference` | string | yes | The item's key. | | `data.resolution` | `done`, `wont_do`, `duplicate`, `cannot_reproduce`, `deferred`, null | yes | Why it closed; null for a move into an open status. | | `data.status` | `backlog`, `ready`, `in_progress`, `in_review`, `in_qa`, `done`, `cancelled` | yes | Where the item is now. | | `data.title` | string | yes | What the item is called. | | `data.work_item_id` | string (uuid) | yes | The item's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD2", "type": "work_item.transitioned", "created_at": "2026-11-05T15:00:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "previous_status": "in_qa", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-142", "resolution": "done", "status": "done", "title": "Tier upgrade email", "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # work_item.updated Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/events/work-item/work-item-updated An item's fields change. ### When this fires Any of the item's own columns change: the title, the prose, the kind, the parent, the epic, the priority, the people, the estimates or the dates. A status move, a hand-over and a replace of the criteria raise their own events, not this one. `changes` holds one entry per column that changed, each with the value before and after. ### Related events - `work_item.transitioned`, `work_item.assigned`, `work_item.criteria_changed`: the moves that are deliberately not updates. ### Request headers | Header | Description | | --- | --- | | `ZG-Signature` | `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `"<t>.<raw body>"` under the endpoint's secret. Verify it before acting, and refuse a `t` more than 300 seconds from now. | | `ZG-Event-Id` | The event's ULID, bare. The envelope's `id` is the same ULID prefixed `evt_`, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id. | | `ZG-Event-Name` | The event name, the same as the envelope's `type`. | | `Content-Type` | Always `application/json`. | | `User-Agent` | Always `ZuseBifrost-Webhooks/1.0`. | ### Delivered body | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on. | | `type` | `work_item.updated` | yes | The event name, always `work_item.updated` here. | | `created_at` | string (date-time) | yes | When the event was emitted, server time, ISO 8601. | | `api_version` | string | yes | The contract version of the payload shape, `2026-11-01` today. A breaking change to a shape ships under a new version. | | `project_id` | string \| null (uuid) | yes | The project the event belongs to; null for the team-level events (teams, roles, tokens and a team-wide AI budget). | | `data` | object | yes | The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence. | | `data.changes` | object | yes | The columns that changed, each `{ "from": …, "to": … }`. | | `data.project_id` | string (uuid) | yes | The project the item belongs to. | | `data.reference` | string | yes | The item's key. | | `data.title` | string | yes | What the item is called after the change. | | `data.work_item_id` | string (uuid) | yes | The item's id. | #### Example: Delivery ```json { "id": "evt_01K8QD4N3M7Y2K4N6P9R1T3VD1", "type": "work_item.updated", "created_at": "2026-10-28T11:30:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "changes": { "priority": { "from": "medium", "to": "high" }, "story_points": { "from": null, "to": "5" } }, "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "reference": "CRM-142", "title": "Tier upgrade email", "work_item_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" } } ``` ### Your response - **2XX**: Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored. - **default**: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from Settings → Webhook Deliveries or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed. --- # Webhooks & Events Source: https://alm-docs.labs.zuselab.dev/webhooks/v1 Outbound webhooks let any HTTPS endpoint subscribe to the events Zuse Bifrost emits. When something happens, a project is created, a member joins a team, a role changes, an API token is minted, a budget runs low, Zuse Bifrost `POST`s a signed JSON envelope to every endpoint subscribed to that event. The families grow with the product: planning, build, test, release and maintenance events join the catalogue as each module lands, and every one is documented on its own generated page. ## Lifecycle 1. **Register an endpoint** in **Settings → Webhooks** or through the REST API, with a name, an `https://` URL and the events it should receive. The response shows the signing secret once. 2. **An event fires inside Zuse Bifrost.** Every state change goes through one action, and the action announces it; a page, an API call and an MCP tool that make the same change raise the same event. 3. **Fan-out.** The dispatcher finds every active endpoint subscribed to the event within the team the event happened in (and, for a project event, the endpoints narrowed to that project), writes one delivery row per endpoint in a single transaction, and queues each on the webhooks queue. 4. **Delivery.** The worker posts the signed envelope. A `2xx` response marks the delivery `delivered`; a non-2xx response or a timeout marks it `failed` and schedules the next attempt. 5. **Retry ladder.** Up to 8 attempts over roughly four days: `10s, 30s, 2m, 10m, 1h, 6h, 24h, 72h`. 6. **Dead letter.** On the eighth failure the delivery is marked `dead`. After 20 consecutive failures the endpoint is switched off until somebody resumes it. ## What lives in every payload ```json { "id": "evt_01HX...", "type": "project.created", "created_at": "2026-11-02T10:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "...event-specific..." } } ``` - `id` is a ULID, unique per event and shared by every delivery of that event, so a consumer subscribed through two endpoints can de-duplicate across them. - `type` matches one of the catalogue entries; the [event reference](/webhooks/v1/events) has a page per event with its payload, field by field. - `created_at` is ISO 8601 server time, not the arrival time at your endpoint. - `api_version` locks the event shape. Additive fields keep the version; a field that changes meaning ships under a new one. - `project_id` is null for team-scoped events (`client.*`, `team.*`, `role.*`, `token.*`, a team-wide budget alert). - `data` is the event-specific payload. Its keys are declared on the event itself and held to the code by the application's own test suite, so the event pages never describe a field that does not arrive. ## What lives in every request header - `Content-Type: application/json` - `User-Agent: ZuseBifrost-Webhooks/1.0` - `ZG-Signature: t=<unix>,v1=<hex>`, an HMAC-SHA256 over `"<t>.<raw_body>"`. See [signature verification](/webhooks/v1/signature-verification). - `ZG-Event-Id: <ulid>`, the bare ULID. `payload.id` is the same ULID prefixed with `evt_`, so the two are not byte-equal; strip the prefix before comparing. - `ZG-Event-Name: <type>`, which mirrors `payload.type`. <Callout type="warn"> **Reject any request where `|now - t| > 300`.** The five-minute tolerance window is the replay-attack defence. It is the consumer's job to enforce it; Zuse Bifrost enforces the same window in its own verify helper for parity. </Callout> ## Ordering guarantees - Deliveries are **not strictly ordered**. A `project.status_changed` may arrive before the `project.updated` that preceded it under retry. - Consumers should be idempotent on the event id and accept out-of-order delivery. Use the most recent event per resource when reconciling state, or read the resource back through the API. ## Where to go next <Cards> </Cards> --- # Webhooks Quickstart Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/quickstart Zuse Bifrost delivers webhook POSTs to any publicly reachable HTTPS URL you register. This page walks the happy path from zero. ## Register a target [step] In the app, open **Settings → Webhooks** and add an endpoint: a name, the `https://` URL, the events to subscribe, and optionally the project it should be limited to and the addresses it may resolve to. Over the REST API the same registration is one call on the API host, behind the `webhook-endpoint:create` ability: ```http POST /v1/webhook-endpoints HTTP/1.1 Authorization: Bearer $ZUSEGENIUS_TOKEN Idempotency-Key: 1b4e28ba-2fa1-11d2-883f-0016d3cca427 Content-Type: application/json { "name": "CI listener", "url": "https://hooks.example.com/zusegenius", "events": ["project.created", "project.status_changed"] } ``` The response includes a one-time `secret` field. Store it: you need it to verify signatures and you cannot retrieve it again. <Callout type="info"> The URL has to be `https://` on a public host. A loopback, private or link-local address, a local name, or a public name that resolves to a private address is refused at registration, and so is a name that does not resolve at all. See [security](/webhooks/v1/security). </Callout> ## Receive the POST [step] Every delivery lands on your URL as a JSON POST: ```http POST /zusegenius HTTP/1.1 ZG-Event-Id: 01HXDELIVERY000000000TST ZG-Event-Name: project.created ZG-Signature: t=1762041600,v1=4a2b... Content-Type: application/json User-Agent: ZuseBifrost-Webhooks/1.0 {"id":"evt_01HX...","type":"project.created",...} ``` ## Verify the signature [step] Every payload is signed with HMAC-SHA256 over `<timestamp>.<raw_body>`. The [signature verification](/webhooks/v1/signature-verification) page has the recipe in three languages. The short form in Node: ```ts function verify(header: string, body: string, secret: string): boolean { const [tPart, v1Part] = header.split(","); const t = tPart.split("=")[1]; const v1 = v1Part.split("=")[1]; const expected = crypto .createHmac("sha256", secret) .update(`${t}.${body}`) .digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)); } ``` Reject the request if the signature does not match or the timestamp is more than five minutes old. ## Respond 200 quickly [step] Return `200 OK` well inside the 30-second budget. Anything else counts as a failure and triggers a retry. If your work takes longer, enqueue a background job and respond immediately. ## De-duplicate on the event id [step] The `ZG-Event-Id` header (and the `id` in the payload, with its `evt_` prefix) is unique per event. Store it and short-circuit if you have seen it before: retries can make the same event arrive more than once, and two endpoints subscribed to the same event each receive it. ## Prove it works [step] Before any real event fires, send a test delivery from the endpoint's menu in **Settings → Webhooks**. It posts a synthetic `project.created` envelope whose `data` carries `synthetic: true`, signed with the current secret and routed through the same queue as a real event, so DNS, TLS and your verification are exercised end to end. The delivery and its outcome appear in **Settings → Webhooks → Deliveries** like any other. ## What next - [Retries & delivery semantics](/webhooks/v1/retries-and-delivery) - [Security model: secret rotation, the IP allow-list](/webhooks/v1/security) - [Testing & debugging](/webhooks/v1/testing-and-debugging) --- # Retries & Delivery Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/retries-and-delivery Zuse Bifrost treats webhook delivery as at-least-once. If your endpoint is unreachable or responds non-2xx, the worker retries on the schedule below. ## Retry schedule Every delivery gets up to **8 attempts**. The back-off between them is exponential: | Attempt | Wait before retry | | --------- | --------------------------- | | 1 | immediate | | 2 | 10 seconds | | 3 | 30 seconds | | 4 | 2 minutes | | 5 | 10 minutes | | 6 | 1 hour | | 7 | 6 hours | | 8 | 24 hours | | (after 8) | 72 hours before dead-letter | Cumulatively, an endpoint has about **four days** of retries before Zuse Bifrost gives up. That covers every reasonable outage short of a permanent DNS change. ## Dead-lettering After the eighth failure the delivery row is marked `dead` and the worker stops retrying automatically. Dead rows stay in the log so the team can inspect them in **Settings → Webhooks → Deliveries** and either retry by hand or pull the payload out for debugging. Dead rows are kept until somebody prunes them; delivered rows are pruned after 90 days. The **Retry** action on a delivery re-queues a failed or dead row as pending, resetting its attempts to zero and clearing the recorded response, so the log shows the replay's own outcome. A pending or delivered row cannot be retried: resetting a pending row would race the worker already holding it, and resetting a delivered row would post the event to your server twice. ## Auto-disable on sustained failure If an endpoint reaches **20 consecutive failures** (across many events, not just 20 retries of one), Zuse Bifrost switches it off automatically and records when. This stops the worker hammering a permanently broken URL. When the endpoint is resumed, in **Settings → Webhooks** or through the API, its consecutive-failure streak resets and fresh deliveries flow. Pausing an endpoint yourself has the same effect on deliveries: a delivery picked up for a paused endpoint is dead-lettered without being posted, and can be replayed once the endpoint is resumed. ## Response-time budget Your handler has **30 seconds** per delivery. Anything longer counts as a failure and triggers the next retry. If the work itself takes longer, the canonical pattern is: 1. Verify the signature. 2. Enqueue your own background job with the payload. 3. Respond `200 OK` immediately. ## What counts as a failure - Any non-2xx HTTP status. - Connection timeouts (30 seconds). - TLS handshake failures. - DNS resolution failures. 5xx counts and retries. 4xx counts and retries too: even though 4xx usually means your code rejected the payload, verification can be brittle during a secret rotation and a retry usually recovers. If you deliberately want to drop an event, respond 200 and ignore it on your side. ## Replaying dead deliveries in bulk The deliveries page has a **Replay dead** action that re-queues every dead row whose dead-letter time is within the last 24 hours, up to 500 rows per click. Use it after you have deployed a fix. ## Related - [Signature verification](/webhooks/v1/signature-verification) - [Security model](/webhooks/v1/security) - [Testing & debugging](/webhooks/v1/testing-and-debugging) --- # Webhook Security Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/security Outbound webhooks are the only way Zuse Bifrost talks to your infrastructure unprompted. Getting the security right prevents spoofing and replay. ## Trust only signed, fresh payloads Every delivery carries: - `ZG-Signature: t=<unix>,v1=<hex_hmac_sha256(secret, "t.body")>` - `ZG-Event-Id: <ulid>`, unique per event. - `ZG-Event-Name: <event>`, which matches `payload.type`. Do three things on every inbound POST: 1. **Verify the signature** with a constant-time comparison (`crypto.timingSafeEqual`, `hash_equals`, `hmac.compare_digest`). 2. **Reject stale deliveries**: if `abs(now - t) > 300` (five minutes), drop the request even when the signature matches. This is your replay window. 3. **De-duplicate on the event id**: store seen ids for at least 24 hours. Retries cause the same event to arrive more than once. ## Secret rotation Rotate a webhook secret the moment a developer leaves, a laptop is lost or CI logs leak. In the app: **Settings → Webhooks**, the endpoint's menu, **Rotate secret**. The same action exists on the REST API and as an MCP tool, each behind the `webhook-endpoint:update` ability. The new secret is shown **once**; you cannot fetch it again. Deploy the new secret to your handler before you rotate, or deliveries between the rotation and your deploy will fail verification. ### There is no grace window Rotation replaces the secret **in place**. The old secret stops verifying the instant the rotation completes, and every delivery after that point carries a single `v1=` signature made with the new one. There is no dual-signing and no overlap period. <Callout type="warn"> Deploy the new secret before you rotate, not after. Anything delivered between the rotation and your deploy fails verification and is retried on the normal back-off schedule, so a slow deploy costs you time, not events. </Callout> ## Who may change an endpoint Every member of the team can see the team's endpoints and their deliveries. Only the member who registered an endpoint and the team's owner can rotate, pause, resume, test or delete it; another member's attempt is refused with a message that says so, and over the API the same call answers `403`. A guest of a project never sees webhooks at all. ## IP allow-list `allowed_ips` is an optional list of IPv4 or IPv6 addresses and CIDR ranges (for example `198.51.100.7` or `2001:db8::/32`) accepted when registering an endpoint. It is enforced on every delivery. Before a delivery is posted, the worker resolves the endpoint's hostname (or reads the literal address in the URL) and checks **every** address it gets back against the list. If any address falls outside it, the delivery is dead-lettered without being posted, with the refused address recorded in the delivery's response excerpt (`Endpoint host resolved to 203.0.113.10, outside its allowed IPs.`). Correct the list or the DNS record, then replay the dead deliveries from the deliveries page. <Callout type="info"> The list restricts where Zuse Bifrost will deliver; it does not authenticate what arrives. Keep verifying the signature on your side, and firewall the handler to the Zuse Bifrost egress if you need inbound filtering too. A hostname that does not resolve at all is not refused by the list; the connection failure walks the normal retry ladder. </Callout> ## TLS and public hosts Zuse Bifrost enforces full certificate-chain validation (no self-signed bypasses) and TLS 1.2 at minimum. <Callout type="info"> **Only `https://` targets on public hosts are accepted at registration.** Plain `http://` URLs are refused, and so is any host that is a loopback, private, link-local or reserved address, or a public name that resolves to one, because the delivery worker would otherwise be reachable as a proxy into the platform's own network. A name that does not resolve at registration is refused too. </Callout> Certificate pinning is not supported. If you need it, terminate TLS on a proxy you control and connect from the proxy to your application over an internal network. ## Data exposure Webhook payloads carry Zuse Bifrost identifiers, keys, names, statuses and roles. They **never** include: - API tokens, webhook secrets or integration credentials. - Telegram chat identifiers or any other messaging identifier. - Time logs, cost rates or internal comments. - A guest's personal details beyond the user id their membership row names. The page broadcast that mirrors each event to open browser tabs is scrubbed a second time: any key that looks like a secret, token, password, key or signature is dropped before it reaches the channel, and an oversized payload is replaced by a marker rather than sent. ## Related - [Signature verification](/webhooks/v1/signature-verification) - [Testing & debugging](/webhooks/v1/testing-and-debugging) --- # Signature Verification Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/signature-verification Every delivery carries a `ZG-Signature: t=<unix>,v1=<hex>` header. Verify it before acting on the payload, or anyone who guesses the endpoint's URL can forge events. ## Scheme ``` ZG-Signature: t=<unix_ts>,v1=<hex(hmac_sha256(secret, "<t>.<raw_body>"))> ``` There is exactly one signature per delivery, always `v1=`, and it is always made with the endpoint's current secret. Accept the request if that signature matches, within a five-minute timestamp tolerance. <Callout type="info"> The implementations below also accept a `v0=` value and a list of secrets. Zuse Bifrost never sends `v0=`; that tolerance exists so **you** can hold two secrets across a [rotation](/webhooks/v1/security#secret-rotation), which is necessary because rotation is instant and has no grace window. </Callout> ## Reference implementations ### Node (Express) `secrets` accepts a string or an array. Hold both the current and the next secret across a rotation so a single deploy rides through the instant cutover. ```ts export function verifySignature( header: string, rawBody: string, secrets: string | string[], toleranceSeconds = 300, ): boolean { const parts = Object.fromEntries( header.split(",").map((kv) => kv.split("=").map((s) => s.trim())), ); if (!parts.t) return false; const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)); if (skew > toleranceSeconds) return false; const signed = `${parts.t}.${rawBody}`; const secretList = Array.isArray(secrets) ? secrets : [secrets]; const candidates = [parts.v1, parts.v0].filter(Boolean) as string[]; for (const secret of secretList) { const expected = crypto .createHmac("sha256", secret) .update(signed) .digest("hex"); const expectedBuf = Buffer.from(expected, "hex"); for (const candidate of candidates) { if (candidate.length !== expected.length) continue; const candidateBuf = Buffer.from(candidate, "hex"); if (crypto.timingSafeEqual(candidateBuf, expectedBuf)) { return true; } } } return false; } ``` ### PHP (Laravel) Wire it as a route middleware so every webhook controller stays free of verification noise. Configure the secrets under `services.zusegenius.webhook_secrets`, an array that holds both the old and the new value during a rotation. ```php title="config/services.php" 'zusegenius' => [ 'webhook_secrets' => array_filter([ env('ZUSEGENIUS_WEBHOOK_SECRET'), env('ZUSEGENIUS_WEBHOOK_SECRET_PREVIOUS'), ]), ], ``` ```php title="app/Http/Middleware/VerifySignature.php" namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Config; use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Response; class VerifySignature { public function handle(Request $request, Closure $next, int $toleranceSeconds = 300): Response { $parts = Str::of($request->header('ZG-Signature', '')) ->explode(',') ->mapWithKeys(function (string $kv): array { [$k, $v] = array_pad(explode('=', trim($kv), 2), 2, ''); return [trim($k) => trim($v)]; }); abort_if(blank($parts->get('t')), 400, 'missing timestamp'); abort_if( abs(Carbon::now()->getTimestamp() - (int) $parts->get('t')) > $toleranceSeconds, 400, 'timestamp outside tolerance', ); $signed = $parts->get('t').'.'.$request->getContent(); $candidates = collect([$parts->get('v1'), $parts->get('v0')])->filter(); $secrets = collect(Config::get('services.zusegenius.webhook_secrets', []))->filter(); $valid = $secrets->contains( fn (string $secret): bool => $candidates->contains( fn (string $candidate): bool => hash_equals(hash_hmac('sha256', $signed, $secret), $candidate), ), ); abort_unless($valid, 400, 'bad signature'); return $next($request); } } ``` ```php title="routes/web.php" Route::post('/webhooks/zusegenius', WebhookController::class) ->middleware(VerifySignature::class); ``` ### Python (Flask) ```python import hashlib import hmac import time from collections.abc import Iterable def verify_signature( header: str, raw_body: bytes, secrets: str | Iterable[str], tolerance: int = 300, ) -> bool: parts: dict[str, str] = {} for kv in header.split(","): k, _, v = kv.partition("=") parts[k.strip()] = v.strip() if "t" not in parts: return False if abs(int(time.time()) - int(parts["t"])) > tolerance: return False signed = f"{parts['t']}.{raw_body.decode()}".encode() secret_list = [secrets] if isinstance(secrets, str) else list(secrets) candidates = [c for c in (parts.get("v1"), parts.get("v0")) if c] for secret in secret_list: expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() for candidate in candidates: if hmac.compare_digest(candidate, expected): return True return False ``` ## Common mistakes - **Parsing the body as JSON first.** Canonicalisation differs between JSON libraries; compute the HMAC against the **raw** body bytes. - **Comparing with `==`.** Timing-safe comparison is required. Use `crypto.timingSafeEqual`, `hash_equals` or `hmac.compare_digest`. - **Ignoring `t`.** Without the timestamp check, a captured payload can be replayed forever. ## Rotating secrets - Rotating from the endpoint's menu returns a fresh secret and replaces the old one **immediately**. There is no dual-signing and no grace window. - Every delivery carries exactly one `v1=` signature. The header never contains a `v0=` value. - Because the cutover is instant, the safe order is: deploy a handler that accepts **both** the current and the next secret, then rotate, then drop the old value. The reference implementations above take an array of secrets for exactly this reason; the array is how _you_ create the overlap, since the platform does not. - Deliveries signed with the new secret while your handler still only knows the old one fail verification and are retried on the normal back-off schedule. <Callout type="warn"> **Do not store the secret beside the webhook URL in version control.** Treat it with the same care as an API token. Once rotated, the old secret is unrecoverable. </Callout> --- # Testing & Debugging Webhooks Source: https://alm-docs.labs.zuselab.dev/webhooks/v1/testing-and-debugging You do not need to wait for a real project to change to exercise a webhook endpoint. Zuse Bifrost has three tools for development and on-call. ## Fire a synthetic delivery From the app: **Settings → Webhooks**, the endpoint's menu, **Send test delivery**. The REST API and the MCP server offer the same action behind the `webhook-endpoint:update` ability. The test delivery: - Creates a pending delivery row typed as `project.created`. - Signs the payload with the current secret. - Routes it through the same queue that real deliveries use. - Shows up in the delivery log at once, so you can watch its progress. The `data` block is a marker, not a realistic event body: ```json { "id": "evt_01HXDELIVERY000000000TST", "type": "project.created", "created_at": "2026-11-02T10:05:00+00:00", "api_version": "2026-11-01", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "data": { "synthetic": true, "endpoint_id": "c62a08f4-1b7d-4e35-9860-a37f5d21e0b9" } } ``` Use it to verify DNS, TLS and HMAC verification end to end. It will **not** exercise your field-mapping logic, and because it is always typed `project.created` it never exercises any other event's shape. Branch on `data.synthetic` to skip business logic on test deliveries. Only the member who registered the endpoint, or the team owner, can fire a test (or rotate, pause, resume and delete it). Other team members see the endpoint but the menu actions answer with a refusal; over the API the same call returns `403`. ## Inspect the delivery log Every delivery, real or synthetic, shows up in **Settings → Webhooks → Deliveries** with: - The endpoint's name - The event name - The status (`pending`, `delivered`, `failed` or `dead`) - The HTTP status your handler returned - The number of attempts so far - When it was created Filter by status with the tabs at the top; **Dead-lettered** finds the rows that exhausted their retries. Opening a row shows the full envelope that was posted, the response excerpt (the first 4 KB) your handler returned or the reason the worker refused to post, and the time of the next attempt. ## Use a request inbox for a quick inspection When you need to see exactly what Zuse Bifrost posts: 1. Open a request-inbox service such as webhook.site and copy the unique inbox URL. 2. Register an endpoint pointing at it with whatever events you want to see. 3. Trigger the flow, or fire a test delivery as above. 4. The inbox shows the raw headers, the body and your response. Delete the endpoint when you are done so deliveries do not keep flowing to a public inbox. ## Retry a failed or dead row by hand From the deliveries page, a row's **Retry** action resets it to `pending` with zero attempts, clears the recorded response and queues the worker again immediately. A pending or delivered row cannot be retried, so a second click on a row the worker already picked up is refused rather than doubled. **Replay dead** re-queues every row dead-lettered in the last 24 hours, up to 500 at a time. ## Common failure modes | Symptom | Likely cause | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `HTTP 401` or `403` from your handler | Signature verification rejecting the payload: check you are using the latest secret and the raw body, not re-parsed JSON. | | `Connection refused` | The target is down, or behind a firewall the delivery worker cannot reach. | | `SSL handshake failed` | An expired certificate or a private CA. Zuse Bifrost validates the full chain. | | `Endpoint host resolved to …, outside its allowed IPs.` | The hostname moved outside the endpoint's allow-list; fix the list or the record and replay. | | `Endpoint inactive or deleted before delivery.` | The endpoint was paused, auto-disabled or removed while the delivery waited; resume it and replay. | | Delivered, but downstream never fires | Your handler returned 200 and crashed before enqueuing the work. Queue it first, then respond. | ## Related - [Retries & delivery](/webhooks/v1/retries-and-delivery) - [Signature verification](/webhooks/v1/signature-verification) - [Security model](/webhooks/v1/security) --- # Ability Reference Source: https://alm-docs.labs.zuselab.dev/workspace/abilities This page is the reference to keep open while setting up [roles](/workspace/roles) and [groups](/workspace/groups). It lists every permission Zuse Bifrost grants, what it governs, and a plain sentence on what a member holding it can do. The same codes are the abilities a [personal access token](/workspace/api-tokens) can carry, so a token with `work-item:view` reads work items and nothing else about them. ## How permissions are structured Every permission has two parts, joined with a colon: ``` {entity}:{action} ``` - **Entity**: the thing the permission acts on, such as `project`, `work-item` or `webhook-endpoint`. - **Action**: what may be done with it: `view-any`, `view`, `create`, `update`, `delete`, or a named action of that entity such as `transition` or `sign-off`. There are **50 entities** and **265 permissions** in all. Most entities carry the same **5 actions**; the ones with a lifecycle add named actions beside them (`submit` and `approve` on the planning artefacts, `start` and `close` on a sprint, `execute` and `sign-off` on a test run), and a few carry only `read`, `use` or `manage` because there is nothing to create or delete. Every count on this page is written from the permission catalogue on each build. ## The five actions | Code | Meaning | | ---------- | -------------------------------------------------------------- | | `view-any` | See the list of every item of the entity the person may reach. | | `view` | Open one item and read its details. | | `create` | Add a new item. | | `update` | Edit an existing item. | | `delete` | Remove an item, or archive it where the record is kept. | <Callout type="info"> `view-any` is listing access; `view` is detail access. Most roles want both, and the eight default roles grant both wherever they grant one, but splitting them lets a custom role open a record it is pointed at without browsing all of them. </Callout> <Callout type="info" title="A permission is a ceiling, not a key to every project" > A permission says what a member may do in the team; which projects they may do it in is decided by project visibility and membership, and a guest of one project never holds a team permission at all. [Members and guests](/workspace/members-and-guests) has the rules. </Callout> ## Workspace ### Team (`team`) The team itself: its name, its existence and its settings. | Permission | What it lets the member do | | --------------- | ---------------------------------------------- | | `team:view-any` | See the list of teams they belong to. | | `team:view` | Open a team and read its settings and members. | | `team:create` | Create a new team, which they then own. | | `team:update` | Rename a team and change its settings. | | `team:delete` | Delete a team with everything it holds. | ### Team member (`team-member`) The people in a team: inviting staff and guests, changing a role, removing someone. | Permission | What it lets the member do | | ------------------------- | ---------------------------------------------------------------------------------------- | | `team-member:view-any` | See the roster of the team's members and pending invitations. | | `team-member:view` | Open a member's row and read their role, groups and last activity. | | `team-member:invite` | Invite a person by email with a role, or a client stakeholder as a guest of one project. | | `team-member:remove` | Remove a member from the team, or cancel a pending invitation. | | `team-member:update-role` | Change the role a member holds in the team. | ### Role (`role`) The named sets of permissions a team hands to its members. | Permission | What it lets the member do | | --------------- | --------------------------------------------------- | | `role:view-any` | See every role of the team. | | `role:view` | Open a role and read the permissions it carries. | | `role:create` | Add a custom role with a chosen set of permissions. | | `role:update` | Change a role's name, description and permissions. | | `role:delete` | Delete a custom role nobody holds any more. | ### Group (`group`) Named sets of permissions handed to several members on top of their roles. | Permission | What it lets the member do | | ---------------- | -------------------------------------------------- | | `group:view-any` | See every group of the team. | | `group:view` | Open a group and read its permissions and members. | | `group:create` | Add a group with a chosen set of permissions. | | `group:update` | Change a group's name, permissions and members. | | `group:delete` | Delete a group; its members keep their roles. | ### API token (`token`) Personal access tokens for the REST API, the MCP server and pipelines. | Permission | What it lets the member do | | ---------------- | ------------------------------------------------------------- | | `token:view-any` | See the list of their own tokens. | | `token:view` | Open one of their tokens and read its abilities and last use. | | `token:create` | Mint a token carrying a subset of their own abilities. | | `token:update` | Change a token's name. | | `token:delete` | Revoke a token so its holder's next request is refused. | ### Webhook endpoint (`webhook-endpoint`) The URLs that receive signed event payloads. | Permission | What it lets the member do | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `webhook-endpoint:view-any` | See the team's webhook endpoints. | | `webhook-endpoint:view` | Open an endpoint and read its subscriptions and recent deliveries. | | `webhook-endpoint:create` | Register an endpoint and receive its signing secret. | | `webhook-endpoint:update` | Change an endpoint's name, URL, events, allowed addresses; pause or resume it; rotate its secret; send it a test delivery. | | `webhook-endpoint:delete` | Delete an endpoint and stop every delivery to it. | ### Webhook delivery (`webhook-delivery`) The log of every event posted to an endpoint and what became of it. | Permission | What it lets the member do | | --------------------------- | --------------------------------------------------------------------- | | `webhook-delivery:view-any` | See the delivery log, with its filters. | | `webhook-delivery:view` | Open a delivery and read the payload, the attempts and the responses. | | `webhook-delivery:retry` | Retry a failed or dead delivery, or every dead one at once. | ### Integration (`integration`) The connections to outside services: the Git hosting app, the Telegram bot's team settings, CI tokens. | Permission | What it lets the member do | | -------------------- | ---------------------------------------------------------- | | `integration:manage` | Connect, configure and disconnect the team's integrations. | ### Activity (`activity`) The team's activity log: every change, who made it and where it came from. | Permission | What it lets the member do | | --------------- | ------------------------------------------------------------------- | | `activity:read` | Read the team's activity log and a record's field-by-field history. | ### Dashboard (`dashboard`) The overview pages that sum up the team's and a project's work. | Permission | What it lets the member do | | ---------------- | --------------------------------------------- | | `dashboard:read` | Open the overview and the project dashboards. | ### Report (`report`) The reports hub: velocity, cycle time, test pass rates, SLA compliance, hours by client. | Permission | What it lets the member do | | ------------- | --------------------------------- | | `report:read` | Open the reports and export them. | ### AI (`ai`) Heimdall and every AI assist, and the team's monthly AI budget. | Permission | What it lets the member do | | ------------------- | ---------------------------------------------------------------------------------------------- | | `ai:use` | Use the Heimdall panel, the AI assists and the quality reviews, within the rest of their role. | | `ai:manage-budgets` | Change the team's monthly AI budget and its warning line under Settings → AI. | ## Clients and the project registry ### Client (`client`) The client accounts projects are run for. | Permission | What it lets the member do | | ----------------- | ------------------------------------------------------------------- | | `client:view-any` | See the list of clients. | | `client:view` | Open a client and read its projects, stakeholders and open tickets. | | `client:create` | Add a client. | | `client:update` | Edit a client's details, contacts and status. | | `client:delete` | Remove a client that has no projects. | ### Project (`project`) The projects of the team, each the record of one engagement. | Permission | What it lets the member do | | ------------------ | --------------------------------------------------------------------------------------------------------- | | `project:view-any` | See the projects of the team they may see: every team-visible project plus the ones they are a member of. | | `project:view` | Open a project and read its registry, record and dashboard. | | `project:create` | Create a project, alone or through the intake wizard. | | `project:update` | Edit a project's details, status, leads and settings. | | `project:delete` | Archive, restore or delete a project and everything in it. | ### Project member (`project-member`) Who is on a project beyond the team, with which project role. | Permission | What it lets the member do | | ------------------------- | ----------------------------------------------------------------------------------------- | | `project-member:view-any` | See a project's members and their project roles. | | `project-member:view` | Open a project member's row. | | `project-member:create` | Add a team member to a project with a role override, or invite a client as a guest of it. | | `project-member:update` | Change a project member's project role. | | `project-member:delete` | Remove a member or a guest from a project. | ### Component (`component`) A project's applications: the web app, the API, the mobile app, the services. | Permission | What it lets the member do | | -------------------- | ------------------------------------------------------------------- | | `component:view-any` | See a project's components. | | `component:view` | Open a component and read its stack, repositories and environments. | | `component:create` | Add a component to a project. | | `component:update` | Edit a component's name, kind, stack and owner. | | `component:delete` | Remove a component. | ### Environment (`environment`) Where each component runs: development, staging, UAT, production and the rest. | Permission | What it lets the member do | | ---------------------- | ------------------------------------------------------------------------------ | | `environment:view-any` | See a project's environments and their health. | | `environment:view` | Open an environment and read its URLs, branch, version and current deployment. | | `environment:create` | Register an environment. | | `environment:update` | Edit an environment's URLs, branch, protection and health check. | | `environment:delete` | Remove an environment. | | `environment:deploy` | Deploy into an environment, or record a deployment made elsewhere. | | `environment:freeze` | Freeze an environment for a change window, and lift the freeze. | ### Repository (`repository`) The Git repositories connected to a project's components. | Permission | What it lets the member do | | --------------------- | ----------------------------------------------------------------------- | | `repository:view-any` | See a project's connected repositories. | | `repository:view` | Open a repository and read its branches, pull requests and linked work. | | `repository:create` | Connect a repository to a component. | | `repository:update` | Change which component a repository belongs to and its default branch. | | `repository:delete` | Disconnect a repository. | ### Stakeholder (`stakeholder`) The people on the client's side and beyond who sponsor, approve and sign off. | Permission | What it lets the member do | | ---------------------- | --------------------------------------------------------------------- | | `stakeholder:view-any` | See a project's stakeholders. | | `stakeholder:view` | Open a stakeholder and read their role, influence and approval flags. | | `stakeholder:create` | Add a stakeholder. | | `stakeholder:update` | Edit a stakeholder's details, RACI defaults and approval flags. | | `stakeholder:delete` | Remove a stakeholder. | ### Persona (`persona`) The user archetypes a project builds for. | Permission | What it lets the member do | | ------------------ | --------------------------------------------------------------- | | `persona:view-any` | See a project's personas. | | `persona:view` | Open a persona and read its goals, frustrations and behaviours. | | `persona:create` | Add a persona. | | `persona:update` | Edit a persona. | | `persona:delete` | Archive or remove a persona. | ### Glossary (`glossary`) The terms a project defines, linked wherever they appear. | Permission | What it lets the member do | | ------------------- | ------------------------------------------------ | | `glossary:view-any` | See a project's glossary. | | `glossary:view` | Open a term and read its definition and aliases. | | `glossary:create` | Add a term. | | `glossary:update` | Edit a term and its status. | | `glossary:delete` | Remove a term. | ### Data entity (`data-entity`) The data dictionary: the entities a project stores and their fields. | Permission | What it lets the member do | | ---------------------- | ------------------------------------------ | | `data-entity:view-any` | See a project's data dictionary. | | `data-entity:view` | Open an entity and read its fields. | | `data-entity:create` | Add an entity or a field. | | `data-entity:update` | Edit an entity, its fields and its status. | | `data-entity:delete` | Remove an entity or a field. | ### Design reference (`design-reference`) Links and uploads to designs, prototypes, API contracts and diagrams. | Permission | What it lets the member do | | --------------------------- | --------------------------------------------------- | | `design-reference:view-any` | See a project's design references. | | `design-reference:view` | Open a design reference, its viewer or its upload. | | `design-reference:create` | Add a design reference. | | `design-reference:update` | Edit a design reference's title, version and notes. | | `design-reference:delete` | Remove a design reference. | ## Planning ### Epic (`epic`) The large pieces of scope stories hang off. | Permission | What it lets the member do | | --------------- | --------------------------------------------------------------------- | | `epic:view-any` | See a project's epics and the roadmap. | | `epic:view` | Open an epic and read its stories, progress and history. | | `epic:create` | Add an epic. | | `epic:update` | Edit an epic that is not baselined. | | `epic:delete` | Delete an epic. | | `epic:submit` | Submit an epic for review. | | `epic:approve` | Decide a review of an epic: approve it, request changes or reject it. | ### Work item (`work-item`) Stories, tasks, sub-tasks, bugs, spikes and improvements on the backlog and the board. | Permission | What it lets the member do | | ---------------------- | ------------------------------------------------------------------------------ | | `work-item:view-any` | See the backlog, the board and the defects. | | `work-item:view` | Open a work item and read its criteria, links, tests, code, time and comments. | | `work-item:create` | Add a story, task, sub-task, bug, spike or improvement. | | `work-item:update` | Edit a work item's fields, criteria and labels. | | `work-item:delete` | Delete a work item. | | `work-item:transition` | Move a work item to another status, with the note a transition asks for. | | `work-item:assign` | Assign a work item to a person, or take it. | | `work-item:rank` | Reorder the backlog and the board columns. | ### Requirement (`requirement`) Functional and non-functional requirements. | Permission | What it lets the member do | | ---------------------- | ----------------------------------------------------------------------- | | `requirement:view-any` | See a project's requirements. | | `requirement:view` | Open a requirement and read its statement, measure, links and coverage. | | `requirement:create` | Add a requirement. | | `requirement:update` | Edit a requirement that is not baselined. | | `requirement:delete` | Delete a requirement. | | `requirement:submit` | Submit a requirement for review. | | `requirement:approve` | Decide a review of a requirement. | ### Business rule (`business-rule`) The rules the system must enforce, with their conditions, actions and examples. | Permission | What it lets the member do | | ------------------------ | ------------------------------------------------------- | | `business-rule:view-any` | See a project's business rules. | | `business-rule:view` | Open a rule and read its statement, examples and links. | | `business-rule:create` | Add a business rule. | | `business-rule:update` | Edit a rule that is not baselined. | | `business-rule:delete` | Delete a business rule. | | `business-rule:submit` | Submit a business rule for review. | | `business-rule:approve` | Decide a review of a business rule. | ### Use case (`use-case`) The flows an actor takes through the system, step by step. | Permission | What it lets the member do | | ------------------- | -------------------------------------------------------------- | | `use-case:view-any` | See a project's use cases. | | `use-case:view` | Open a use case and read its flows and steps. | | `use-case:create` | Add a use case and its steps, or generate stories from a flow. | | `use-case:update` | Edit a use case and its steps. | | `use-case:delete` | Delete a use case. | ### RAID item (`raid`) Risks, assumptions, issues and dependencies. | Permission | What it lets the member do | | --------------- | ------------------------------------------------------------ | | `raid:view-any` | See a project's RAID log and risk matrix. | | `raid:view` | Open a RAID item and read its mitigation, owner and history. | | `raid:create` | Raise a risk, an assumption, an issue or a dependency. | | `raid:update` | Edit a RAID item and its status. | | `raid:delete` | Delete a RAID item. | ### Decision (`decision`) Architecture, product, process, vendor and security decisions with their options and consequences. | Permission | What it lets the member do | | ------------------- | --------------------------------------------------------------- | | `decision:view-any` | See a project's decisions. | | `decision:view` | Open a decision and read its context, options and consequences. | | `decision:create` | Record a decision. | | `decision:update` | Edit a decision and its status, or supersede it. | | `decision:delete` | Delete a decision. | ### Link (`link`) The traceability links between artefacts: implements, verifies, covers, depends on, blocks and the rest. | Permission | What it lets the member do | | --------------- | ------------------------------------------------------ | | `link:view-any` | See the links of a record and the traceability matrix. | | `link:view` | Open a link. | | `link:create` | Link two artefacts. | | `link:update` | Change a link's kind. | | `link:delete` | Remove a link. | ### Baseline (`baseline`) A frozen set of approved revisions: the scope at a point in time. | Permission | What it lets the member do | | ------------------- | ------------------------------------------------------------------------------ | | `baseline:view-any` | See a project's baselines and compare two. | | `baseline:view` | Open a baseline and read what it froze. | | `baseline:create` | Compose a baseline from approved artefacts. | | `baseline:update` | Edit a baseline's name and notes before it is frozen. | | `baseline:delete` | Delete a baseline that was never frozen. | | `baseline:freeze` | Freeze a baseline, after which its items change only through a change request. | ### Approval (`approval`) The review and sign-off requests on artefacts, releases, documents and runs. | Permission | What it lets the member do | | -------------------------- | ----------------------------------------------------------------------- | | `approval:view-any` | See the approvals inbox and a project's pending approvals. | | `approval:view` | Open an approval request and read its revision, decisions and comments. | | `approval:decide` | Approve, request changes or reject an approval assigned to them. | | `approval:withdraw` | Withdraw an approval request they raised. | | `approval:manage-policies` | Edit a project's approval policies: approvers, rules, quorums and SLAs. | ### Change request (`change-request`) A proposed change to a baselined scope, with its impact and its approval. | Permission | What it lets the member do | | ------------------------- | ----------------------------------------------------------------------------------- | | `change-request:view-any` | See a project's change requests. | | `change-request:view` | Open a change request and read its impact and the artefacts it touches. | | `change-request:create` | Raise a change request. | | `change-request:update` | Edit a change request before it is decided. | | `change-request:delete` | Delete a change request that was never decided. | | `change-request:analyse` | Record the impact analysis: scope, schedule, effort, cost and risk. | | `change-request:approve` | Approve or reject a change request, which unlocks the baselined artefacts it names. | | `change-request:verify` | Verify that an implemented change request delivered what it promised. | ### Comment (`comment`) Comments on any record, with mentions and threads. | Permission | What it lets the member do | | ------------------ | ---------------------------------------------------------------- | | `comment:view-any` | Read the comments on records they can see. | | `comment:view` | Open a comment thread. | | `comment:create` | Write a comment. | | `comment:update` | Edit their own comment. | | `comment:delete` | Delete their own comment, or any comment on records they manage. | | `comment:internal` | Write and read internal comments, hidden from client guests. | ### Attachment (`attachment`) Files pinned on any record: wireframes, specifications, samples. | Permission | What it lets the member do | | --------------------- | ----------------------------------------------------------------------------------- | | `attachment:view-any` | See the files on records they can see. | | `attachment:view` | Open or save a file. | | `attachment:create` | Upload a file onto a record, and remove their own. | | `attachment:update` | Reserved for replacing a file in place; today a file is removed and attached again. | | `attachment:delete` | Remove anyone's file. | ### AI import (`import`) The brief imports the AI drafts a planning scaffold from, and their review queues. | Permission | What it lets the member do | | ----------------- | ----------------------------------------------------------------- | | `import:view-any` | See a project's import batches. | | `import:view` | Open a batch and its review queue. | | `import:create` | Start an import from pasted text, an upload or a knowledge entry. | | `import:update` | Accept, edit, merge or reject the suggestions of a batch. | | `import:delete` | Discard a batch. | ### Knowledge (`knowledge`) What a project teaches: uploaded documents, notes and answers Heimdall and the search read. | Permission | What it lets the member do | | -------------------- | ---------------------------------- | | `knowledge:view-any` | See a project's knowledge entries. | | `knowledge:view` | Open an entry and read it. | | `knowledge:create` | Add an entry or upload a document. | | `knowledge:update` | Edit an entry and its audience. | | `knowledge:delete` | Remove an entry. | ### Document (`document`) Generated documents: BRD, SRS, sprint and status reports, release notes, sign-off sheets, test summaries, post-mortems. | Permission | What it lets the member do | | ------------------- | ------------------------------------------------------------ | | `document:view-any` | See a project's generated documents. | | `document:view` | Open or download a document. | | `document:create` | Draft a document. | | `document:update` | Edit a document's draft sections. | | `document:delete` | Delete a draft document. | | `document:render` | Render a document to its final file. | | `document:finalise` | Finalise a document, after which it is versioned and shared. | ## Build ### Sprint (`sprint`) The timeboxes a project builds in. | Permission | What it lets the member do | | ----------------- | ------------------------------------------------------------------- | | `sprint:view-any` | See a project's sprints and burndowns. | | `sprint:view` | Open a sprint and read its goal, scope, velocity and retrospective. | | `sprint:create` | Plan a sprint and move work into it. | | `sprint:update` | Edit a sprint's goal, dates, capacity and scope. | | `sprint:delete` | Delete a planned sprint. | | `sprint:start` | Start a sprint, which snapshots its commitment. | | `sprint:close` | Close a sprint and decide where unfinished work goes. | ### Time log (`time-log`) The time logged against work items and the weekly timesheets. | Permission | What it lets the member do | | ------------------- | ----------------------------------------------------------- | | `time-log:view-any` | See the time logged on a project and the team's timesheets. | | `time-log:view` | Open a time log. | | `time-log:create` | Log time, by hand or with the timer. | | `time-log:update` | Edit a time log that is not locked. | | `time-log:delete` | Delete a time log that is not locked. | | `time-log:approve` | Approve and lock a member's week. | ## Test ### Test suite (`test-suite`) The folders test cases are organised in. | Permission | What it lets the member do | | --------------------- | ---------------------------- | | `test-suite:view-any` | See a project's test suites. | | `test-suite:view` | Open a suite and its cases. | | `test-suite:create` | Add a suite. | | `test-suite:update` | Rename or move a suite. | | `test-suite:delete` | Delete an empty suite. | ### Test case (`test-case`) The test cases with their steps, data and automation references. | Permission | What it lets the member do | | -------------------- | ---------------------------------------------------------- | | `test-case:view-any` | See a project's test cases and the traceability matrix. | | `test-case:view` | Open a test case and read its steps, coverage and results. | | `test-case:create` | Write a test case, or accept ones the AI drafted. | | `test-case:update` | Edit a test case and its steps. | | `test-case:delete` | Delete a test case. | | `test-case:approve` | Decide a review of a test case. | ### Test plan (`test-plan`) What is tested when, where and by whom. | Permission | What it lets the member do | | -------------------- | --------------------------------------------------- | | `test-plan:view-any` | See a project's test plans. | | `test-plan:view` | Open a plan and read its scope, criteria and cases. | | `test-plan:create` | Compose a test plan. | | `test-plan:update` | Edit a plan and its cases. | | `test-plan:delete` | Delete a draft plan. | | `test-plan:approve` | Approve a test plan so its runs can start. | ### Test run (`test-run`) The executions of a plan on an environment, with their results and defects. | Permission | What it lets the member do | | ------------------- | ------------------------------------------------------------------------- | | `test-run:view-any` | See a project's test runs and their results. | | `test-run:view` | Open a run and read every execution. | | `test-run:create` | Start a run from a plan, or an ad-hoc one. | | `test-run:update` | Edit a run's details and assignments. | | `test-run:delete` | Delete a run that never started. | | `test-run:execute` | Record results step by step, raise defects and import automation results. | | `test-run:sign-off` | Sign off a UAT run as the client's representative. | ## Release ### Release (`release`) The versions composed from done work and shipped to the environments. | Permission | What it lets the member do | | ------------------ | ------------------------------------------------------------------ | | `release:view-any` | See a project's releases and their readiness. | | `release:view` | Open a release and read its scope, checklist, approvals and notes. | | `release:create` | Plan a release and compose its scope. | | `release:update` | Edit a release's scope, dates, checklist and notes. | | `release:delete` | Delete a release that was never released. | | `release:freeze` | Declare the code freeze. | | `release:approve` | Give the go-live approval. | | `release:publish` | Publish the release notes to the client. | | `release:rollback` | Roll a release back. | ### Deployment (`deployment`) What went where: every deployment of a component into an environment. | Permission | What it lets the member do | | --------------------- | ------------------------------------------------------------ | | `deployment:view-any` | See a project's deployments. | | `deployment:view` | Open a deployment and read its pipeline, commit and outcome. | | `deployment:create` | Start a deployment from the app. | | `deployment:update` | Edit a deployment's notes. | | `deployment:delete` | Delete a deployment record. | | `deployment:record` | Record a deployment from a pipeline, as a CI token does. | | `deployment:promote` | Approve a deployment into a protected environment. | ## Maintain ### Incident (`incident`) Production incidents with their severity, SLA timers, updates and post-mortems. | Permission | What it lets the member do | | ------------------------- | ----------------------------------------------------- | | `incident:view-any` | See a project's incidents. | | `incident:view` | Open an incident room and read its timeline. | | `incident:create` | Open an incident. | | `incident:update` | Edit an incident's severity, components and summary. | | `incident:delete` | Delete an incident opened by mistake. | | `incident:command` | Take command of an incident and assign responders. | | `incident:resolve` | Resolve and close an incident. | | `incident:publish-update` | Post an update, including the ones client guests see. | ### Support ticket (`ticket`) The requests raised by clients and staff, with their SLAs and conversations. | Permission | What it lets the member do | | ----------------- | --------------------------------------------------------- | | `ticket:view-any` | See a project's tickets. | | `ticket:view` | Open a ticket and read its conversation. | | `ticket:create` | Raise a ticket. | | `ticket:update` | Edit a ticket's category, priority and assignee. | | `ticket:delete` | Delete a ticket. | | `ticket:respond` | Reply to a ticket and write internal notes on it. | | `ticket:convert` | Convert a ticket into a bug, a story or a change request. | ### Maintenance window (`maintenance-window`) The announced windows an environment is worked on. | Permission | What it lets the member do | | ----------------------------- | ----------------------------------------------------- | | `maintenance-window:view-any` | See a project's maintenance windows and SLA policies. | | `maintenance-window:view` | Open a maintenance window. | | `maintenance-window:create` | Schedule a maintenance window. | | `maintenance-window:update` | Edit or reschedule a window. | | `maintenance-window:delete` | Cancel a window. | ## Related - [Roles](/workspace/roles) — the eight default roles and the role editor - [Groups](/workspace/groups) — permissions handed to several members at once - [API tokens](/workspace/api-tokens) — the same codes as token abilities - [Ability catalogue](/api/v1/abilities) — how the REST API and the MCP server enforce them --- # Activity Log Source: https://alm-docs.labs.zuselab.dev/workspace/activity-log By the end of this page you will know what the activity log records, how to read your team's log and narrow it to what you are looking for, what the origin of an entry tells you, and how to open the full history of one record with every field that changed. Every create, update and delete on a project, a client, a project member and your own account writes one entry: what happened, who did it, when, and the fields that changed with their old and new values. Entries are kept for good and are never edited, so the log is the place to answer "who changed this, and when". ## Before you begin | You need | Why | | ------------------------------------ | ------------------------------------------------------------------------------------------------------- | | A role carrying `activity:read` | Every default role except Guest carries it. Without it the page shows a notice instead of the log. | | To be in the team whose log you want | The page shows your current team. Switch teams from the account menu to read another one you belong to. | ## Where the log lives **Account Settings → Activity** is the team's log: every entry of the team itself and of the projects you may see, newest first. A members-only project you are not a member of contributes nothing to your view, so two people on the same team can see different logs. **Activity History** is the same data for one record, opened from the log's row menu today and from each record's own page as those pages land. It shows that record's entries only, with the changes rendered field by field. ## Read the log [step] 1. Open **Account Settings → Activity**. 2. Read the entries, newest first. Each row says when it happened, what happened, what kind of record it is about, who did it and where the change came from. 3. Use **Search the log** to find words in an entry, such as a project name or "archived". 4. Use the **Origin** list to keep only the changes made by people, by API tokens, by MCP tools, by integrations, by webhooks, by Heimdall or by the system. 5. Page through with the controls under the table. Twenty-five entries fit on a page. ## What an entry carries | Column | What it says | | ---------- | ---------------------------------------------------------------------------------------------------------------- | | **When** | How long ago the change was made. Hover for the exact moment in your timezone. | | **Entry** | What happened, in a sentence that names the record: _Project 'Payments' was updated_. | | **Record** | The kind of record: Project, Client, Project member or Account. | | **By** | The person who made the change, or _System_ when nobody did, and under the name the origin the change came from. | ### Origins | Origin | Meaning | | --------------- | ------------------------------------------------------------------------------------- | | **Human** | Someone signed in to the app made the change. | | **API token** | A request to the REST API carrying a personal access token made it. | | **MCP tool** | An agent connected over MCP made it with one of its tools. | | **Integration** | An inbound delivery did: a Git hosting event, a pipeline report or a Telegram update. | | **Webhook** | A listener reacting to an outbound event made it. | | **Heimdall** | An AI draft someone accepted made it. | | **System** | A scheduled command or a background job made it, with nobody acting. | The origin is recorded on every entry as it is written, so a change made by an agent on your behalf is never mistaken for one you made by hand. ## Open a record's history [step] 1. Press the row's menu and choose **History**. The menu appears on entries about a project, a client or a project member. 2. The **Activity History** flyout lists that record's entries, newest first, each with who made it and its origin. 3. An update shows each changed field with the old value struck through and the new value beside it. Dates read as dates, switches as Yes and No, and an empty value as _None_. 4. Press **Load more** to reveal the next ten entries. <Callout type="info"> History is offered only for records you can open. If the record was deleted or moved out of your view since the entry was written, the page says so instead of opening an empty flyout. </Callout> ## All the fields <Accordions> <Accordion title="Search the log"> Words to find in the entry sentence. The match ignores case and finds the words anywhere in the sentence. Clear the box to see every entry again. Kept in the page address, so a link to a search can be shared. </Accordion> <Accordion title="Origin"> One of Every origin, Human, API token, MCP tool, Integration, Webhook, Heimdall or System. Kept in the page address like the search. </Accordion> </Accordions> ## What happens next - Nothing is written by reading. The log and the history flyout only read what other pages recorded. - Entries are never changed or removed by anyone. A record's deletion is one more entry about it. - The same entries will be readable by integrations through the REST API and by agents over MCP as those surfaces land, filtered to the same records you may see here. ## Permissions | Action | Who may do it | | -------------------------- | --------------------------------------------------------------------------------------- | | Read the team's log | The team owner, and any member whose role or group carries `activity:read`. | | Open a record's history | Anyone who may read the log and may see the record. | | See a members-only project | Its members only, whatever their role; its entries are left out of everyone else's log. | ## Frequently asked <Accordions> <Accordion title="Why does a teammate see entries I do not?"> They are a member of a members-only project you are not in. Its entries are left out of your log entirely. </Accordion> <Accordion title="Why is my own name change in the log?"> Changes to an account are recorded like any other change, so a renamed person or a changed email is explained later. Account entries have no History, because the account is not a record of the team. </Accordion> <Accordion title="Can the log be exported?"> Not yet. An audit export of who approved what, when and from where joins the approvals pages when they land. </Accordion> <Accordion title="An entry says System. Who was that?"> Nobody was signed in: a scheduled command or a background job made the change. The entry sentence says which record it touched. </Accordion> </Accordions> ## Related - [Roles](/workspace/roles): which roles carry `activity:read`. - [Members and guests](/workspace/members-and-guests): why a guest never reads the team log. - [API tokens](/workspace/api-tokens): the tokens behind the _API token_ origin. --- # AI Settings Source: https://alm-docs.labs.zuselab.dev/workspace/ai-settings By the end of this page you will know where to read what the team's AI features have cost this month, how to set the cap that stops them and the line that warns before it, and how to tell which model answers each kind of request. Every Heimdall turn, and later every assist, is costed at the model's list price and summed against the team's **AI budget** for the month. At the cap, every AI feature refuses until the cap is raised or the month ends. This page is where the month is read and the cap is set. ## Before you begin | You want to | You need | | -------------------------------------------- | ------------------------------------------------------------------------------------ | | Read the month, the statement and the models | To belong to the team. Every member sees the page. | | Change the cap or the warning line | To own the team, or a role that carries **Manage AI budgets** (`ai:manage-budgets`). | ## Where it lives Open **Account Settings → AI**. The page acts within the team marked as current; switch teams from the account menu to read another team's month. ## Read the month [step] The **Monthly budget** card opens the page with a badge and two figures: | Badge | Meaning | | ------------------- | ----------------------------------------------------------------------------------- | | **Within budget** | A cap is set and the spend is below the warning line. | | **Nearing the cap** | The spend has passed the warning line; the owner and the budget managers were told. | | **Spent** | The cap is reached; every AI feature refuses until it is raised or the month ends. | | **No cap** | The cap is zero: the spend is shown for the record and nothing ever refuses. | Under the badge, **$12.40 of $100.00** says what the month has cost against its cap, and a line below it reads "12% of the cap spent; the warning goes out at 80%". ## Set the cap and the warning line [step] The form sits beside the figures for a person who may change them; everyone else reads a note saying that only the team owner or a member whose role carries **Manage AI budgets** can change the cap. 1. Type the **Cap for this month (USD)**. Zero means no cap. 2. Type **Warn at (% of the cap)**: the percentage at which the owner and the budget managers are told once. 3. Press **Save budget**. The new cap applies at once. Saving clears the month's warning and exhaustion marks, so the next crossing is announced again; lowering the cap below what is already spent stops Heimdall on its next request. <Callout type="info" title="This month only"> The cap you set applies to the current calendar month. The next month opens from the workspace default on its first call, so a cap raised for a heavy month does not stay raised. </Callout> ## Read the statement [step] **This month** lists every call of the month summed per feature and model, the dearest first: | Column | What it shows | | ----------- | --------------------------------------------------------------------------------------- | | **Feature** | What the calls were for: Heimdall turns, Heimdall routing, and each assist as it lands. | | **Model** | The model that answered. | | **Calls** | How many calls the line sums. | | **In** | The input tokens, the cached part included. | | **Out** | The output tokens, the model's thinking included where it thinks. | | **Cost** | What the line cost, at list price. | A month with no call yet reads "No AI feature has been used this month." **Recent calls** lists the latest calls the team made, newest first, with **When** in your timezone, the **Feature**, the **Model**, the **Person** the call was made for (a scheduled run with no person reads _Scheduled_), and the **Cost** of that one call. ## Read the models [step] **Models** lists which model each job runs on, as a table of **Job**, **Provider** and **Model**. Heimdall picks between the simple and the involved model for each request unless a tier is pinned in its composer; the other jobs name the model their assist runs on once it lands. The table is read-only here: the models are part of the workspace's configuration. ## All the fields <Accordions> <Accordion title="Cap for this month (USD)"> Required. A number from 0 to 1,000,000 with up to two decimals. Zero means no cap: the spend is still filed and shown, and no warning or refusal ever fires. The budget gate before every model call reads this figure. </Accordion> <Accordion title="Warn at (% of the cap)"> Required. A whole number from 1 to 100; a new month starts at 80. When a call takes the month past this share of the cap, the owner and every member whose role carries **Manage AI budgets** are told once, and `ai.budget_warning` is raised. </Accordion> </Accordions> ## What happens next - Crossing the warning line sends the **AI budget** alert to the owner and the budget managers in the Notifications Center, by email, and on Telegram where they route the class there, and raises `ai.budget_warning` to every endpoint subscribed to it. - Reaching the cap sends the same people "AI budget spent", raises `ai.budget_exhausted`, and from the next request on Heimdall answers "This month's AI budget is spent" with a **Raise it** link for a person who may; the REST API and the MCP server answer `AI_BUDGET_EXHAUSTED`. - A change to the cap is written to the [activity log](/workspace/activity-log) on the budget, with the old and the new figure. ## Heimdall on this page Heimdall reads the month's standing with every message, so it can answer without a tool: Try: “How much of the AI budget is left this month?” · “Which feature has cost the most this month?” · “Who used Heimdall most this week?” It cannot change the cap for you: the form on this page is the only way, so that a budget is never raised by the thing it bounds. ## From the API and MCP The budget and the statement have no endpoint or tool yet. The two events, [`ai.budget_warning`](/webhooks/v1/events/ai/ai-budget-warning) and [`ai.budget_exhausted`](/webhooks/v1/events/ai/ai-budget-exhausted), carry the team, the month, the cap, the spend and the percentage to any endpoint subscribed to them. ## Permissions | Action | Permission | | ----------------------------- | ---------------------------------------------------- | | Open the page | Belonging to the team | | Change the cap or the warning | Owning the team, or `ai:manage-budgets` on your role | | Use Heimdall | Owning the team, or `ai:use` on your role | ## Frequently asked <Accordions> <Accordion title="Why does the month show spend when nobody used Heimdall?"> Every AI feature files in the same ledger: the router that picks a model for each Heimdall turn, and each assist as it lands. The statement names the feature on every line. </Accordion> <Accordion title="Can I set a cap per project?"> Not yet. The month is the team's; the statement shows the project each call was about where there was one. </Accordion> <Accordion title="Who is told when the line is crossed?"> The team owner and every member whose role carries **Manage AI budgets**, once per crossing. Nobody else, so a developer's panel simply shows the budget line turning amber. </Accordion> </Accordions> ## Related - [Usage & Budgets](/copilot/usage-and-budgets) — the meter behind the page - [The panel](/copilot/panel#the-budget-line-state-by-state) — what the budget line under the composer reads - [Roles](/workspace/roles) — where **Use AI** and **Manage AI budgets** are granted - [Notifications](/account/notifications) — where the **AI budget** class is delivered --- # API Tokens Source: https://alm-docs.labs.zuselab.dev/workspace/api-tokens By the end of this page you will know what a personal access token is, how to mint one that can do exactly what it needs and nothing more, where its plaintext is shown, and how to revoke it the moment it should stop working. A personal access token is a credential that acts as you from outside the browser: a pipeline reporting a deployment, an AI coding agent reading a story's context over MCP, a script calling the REST API. Each token carries its own set of **abilities** and is scoped to one of your teams and, when you choose, to one project within it. It is a bearer secret: whoever holds the plaintext holds your access, within those limits. ## Before you begin | You need | Why | | ------------------------------- | ------------------------------------------------------------------------------------------------------ | | A team | A token acts within one team you own or belong to. You pick it when minting. | | A project, if you are a guest | A guest's token must be confined to one of the projects they were invited to. | | Somewhere safe to keep a secret | The plaintext is shown once. A password manager or your pipeline's secret store is where it should go. | ## Where tokens live Open **Account Settings → API Tokens**. The page lists your tokens, newest first: | Column | What it shows | | ------------- | -------------------------------------------------------------------------- | | **Name** | The label you gave it, so you can tell the deploy pipeline from the agent. | | **Abilities** | How many abilities it carries, the team and project scopes included. | | **Last used** | When it last authenticated a request, or _never_. | | **Created** | How long ago it was minted. | | Row menu | **Revoke**. | Tokens are personal: the page shows yours, and nobody else's appear on it. ## Create a token [step] 1. Press **Create Token**. 2. Type a **Token name** that says what will hold it, such as _Deploy pipeline_ or _Claude Code on the laptop_. 3. Pick the **Team** the token acts within. With one team the choice is made for you. 4. Pick a **Project** to confine the token to one project, or leave **Whole team** to let it reach every project your role can see in that team. A guest has no whole-team option: the project is required. 5. Tick the **Abilities**. The list is grouped by area, with a **Toggle** on each group and one for everything; the two project-reading abilities start ticked so a token is never minted inert. Tick only what the holder needs. 6. Press **Create token**. A flyout shows the plaintext once, with a copy button. Store it before you press **Done**; afterwards only a hash is kept and nothing can show the value again. If you lose it, revoke the token and mint another. <Callout type="warn"> A token can do everything its abilities allow, as you, within its team and project. Treat the plaintext like a password: never paste it into a chat, a ticket or a repository, and revoke it the moment a device or a colleague that held it is gone. </Callout> ## What a token can and cannot do - **Abilities** follow the same `entity:action` grammar as [role permissions](/workspace/roles). A token with `work-item:view` can read work items and nothing else about them. - **Your own permissions still apply.** A token never does more than you could do yourself in that team; an ability you tick but do not hold answers with a refusal, not a backdoor. - **The team scope** confines the token to one team. The **project scope**, when set, confines it further to one project; a guest's token always carries one. - **Minting more tokens** is not something a token can delegate: a token minted through the API cannot carry `token:create`, and can hand out only abilities it holds itself, so a leaked credential cannot grow. ## Revoke a token [step] 1. Open the row menu and choose **Revoke**. 2. Confirm. The row disappears and every client using that token starts failing on its next request. Revocation cannot be undone and the secret cannot be recovered; mint a new token for a client that should keep working. ## All the fields <Accordions> <Accordion title="Token name"> Required, up to 100 characters. Only a label: it appears in the list, in the activity log and in webhook payloads about the token. </Accordion> <Accordion title="Team"> Required. One of the teams you own or belong to; it starts on your current team. Stored on the token as a `scope:team` ability. </Accordion> <Accordion title="Project"> Optional for staff, required for a guest. One of the projects you can see in your current team. Stored as a `scope:project` ability, and a token that carries it reaches that project only. </Accordion> <Accordion title="Abilities"> At least one. Every ability in the catalogue is offered, grouped by area; unknown or retired abilities are refused. The pre-ticked defaults are `project:view-any` and `project:view`. </Accordion> </Accordions> ## What happens next | Action | Event announced | Payload | | -------------- | --------------- | --------------------------------------------------------------------------------------------- | | Create a token | `token.created` | The abilities, the name, the token id, who owns it, and no expiry (tokens do not expire yet). | | Revoke a token | `token.revoked` | The name, the token id and who owned it. | ## Permissions | Action | Who may do it | | --------------------- | -------------------------------------------- | | See and revoke tokens | You, for your own tokens | | Mint a token | Anyone signed in, within their own teams | | Confine to a project | Anyone who can see the project; a guest must | ## Frequently asked <Accordions> <Accordion title="Where do I use the token?"> As a bearer token on the REST API and as the credential of an MCP connection, and in the pipeline steps that report deployments and test results. The API, MCP and integrations sections of these docs show the exact headers and settings for each. </Accordion> <Accordion title="Can I see the plaintext again later?"> No. Only a hash is stored. Revoke and mint again. </Accordion> <Accordion title="Do tokens expire?"> Not today; a token works until it is revoked. Review the list now and then and revoke what is no longer used. </Accordion> <Accordion title="What does the environment marker in the token mean?"> A token minted on the production workspace starts with `zg_live_`, one minted anywhere else with `zg_test_`. A token is refused outside the environment that minted it, so a test credential can never reach live data. </Accordion> </Accordions> ## Related - [Webhooks](/workspace/webhooks): the other machine surface, pushing events to you. - [Roles](/workspace/roles): the permissions a token's abilities mirror. - [Browser sessions](/account/sessions): the browser side of the same question. --- # Groups Source: https://alm-docs.labs.zuselab.dev/workspace/groups By the end of this page you will know when a group is the right tool, how to create one and choose who is in it, and what its members gain. A group is a named set of permissions handed to several members of one team at once. A member keeps their role and gains whatever their groups grant on top of it. Groups suit the cross-cutting cases a role does not fit: the release crew drawn from developers and QA, the people who may approve on a project manager's behalf, the handful who manage integrations. ## Before you begin | You need | Why | | ---------------- | ----------------------------------------------------------- | | `group:view-any` | To see a team's groups on the page. The owner always can. | | `group:create` | To create a group in a team. | | `group:update` | To edit a group's name or permissions, or pick its members. | | `group:delete` | To delete a group. | ## Where groups live Open **Account Settings → Groups**. The page lists the groups of every team where your role lets you view them, newest first: | Column | What it shows | | --------------- | ------------------------------------------------------------------------------------------------------ | | **Group** | The name, with the code in small type. | | **Team** | Which team the group belongs to. | | **Permissions** | How many permissions the group grants. | | **Members** | How many people are in it. | | Row menu | **Members** and **Edit** when you may update groups in that team, **Delete** when you may delete them. | ## Create a group [step] 1. Press **Create a Group**. The button appears when at least one of your teams lets you create groups. 2. Pick the **Team**, type a **Name** and a **Code**, and tick the **Permissions** the group grants. 3. Press **Create**. The group starts empty; add people from its row menu. The code must be unique within the team and is fixed once created, for the same reason a role's code is. ## Choose the members [step] 1. Open the group's row menu and choose **Members**. 2. Tick the people who should be in the group. The list holds the owner and every member of the group's team, with their addresses, and the people already in the group are ticked. 3. Press **Save Members**. The confirmation says how many were added and how many removed. Only people already in the team can be in one of its groups. Someone outside the team is refused rather than quietly dropped, so a mistake never reads as a save that did less than it said. <Callout type="info"> Saving replaces the membership with exactly what is ticked. To take one person out, untick them and save. </Callout> ## Edit a group [step] 1. Open the row menu and choose **Edit**. The code is shown but not editable. 2. Change the **Name** or the **Permissions** and press **Save**. 3. The group's members gain or lose the permissions the next time they load a page. ## Delete a group [step] 1. Open the row menu and choose **Delete**, then press **Delete Group**. 2. Its members keep their roles and lose only what the group granted. ## All the fields <Accordions> <Accordion title="Team"> Required when creating. One of the teams where you may create groups. </Accordion> <Accordion title="Name">Required, up to 255 characters.</Accordion> <Accordion title="Code"> Required when creating, up to 64 characters of letters, numbers, dashes and underscores, unique within the team, and fixed afterwards. </Accordion> <Accordion title="Permissions"> Any number of codes from the same catalogue roles use. An unknown code is refused. </Accordion> <Accordion title="Members"> Any number of the team's people, the owner included. A person who is not in the team is refused. </Accordion> </Accordions> ## What happens next Groups announce nothing to webhooks today; their effect shows in what members may do. A member's effective permissions are their role's set joined with the sets of every group they are in, and a permission check anywhere in the product reads that union. ## Permissions | Action | Who may do it | | --------------------------- | -------------------------------------------- | | See a team's groups | The owner, or a member with `group:view-any` | | Create a group | The owner, or a member with `group:create` | | Edit a group or its members | The owner, or a member with `group:update` | | Delete a group | The owner, or a member with `group:delete` | ## Frequently asked <Accordions> <Accordion title="Role or group: which should I use?"> A role is what a person is in the team, one per person. A group is an extra set of permissions for a purpose that cuts across roles. If you find yourself creating a role for three people who already have fitting roles, make a group instead. </Accordion> <Accordion title="Can a group take permissions away?"> No. Groups only add. To restrict a person, change their role. </Accordion> <Accordion title="What happens to a group when a member leaves the team?"> Removing someone from the team removes them from its groups too. Deleting the team removes its groups. </Accordion> </Accordions> ## Related - [Roles](/workspace/roles): the permission catalogue and the one role each member holds. - [Members and guests](/workspace/members-and-guests): who can be in a group. --- # Your Workspace Source: https://alm-docs.labs.zuselab.dev/workspace A workspace is made of teams. A team owns projects and holds the people who work on them; every page you open acts within the team marked as current, and the account menu lets you switch between the teams you belong to. Inside a team, a role says what each member may do, and a group hands a further set of permissions to several members at once. ## The pieces | Piece | What it is | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | **Team** | A container for projects and people. Someone owns it; everyone else is a member with a role. A person can be in several teams. | | **Member** | A person invited into the team. Staff hold a role with team-wide permissions; a client stakeholder joins as a guest of one project. | | **Invitation** | An emailed, signed link that creates the account if there is none and adds the person to the team when followed. | | **Role** | A named set of permissions. Every team starts with eight default roles and can add its own. | | **Group** | A named set of permissions handed to several members on top of their roles. | <Callout type="info"> Teams are not tenants of separate workspaces. They are how one company organises its work: one team per department or per client account, with projects living in the team that serves them. </Callout> ## Pages <Cards> </Cards> ## For developers and pipelines <Cards> </Cards> ## Who may do what The owner of a team may do everything in it. Every other member is limited by their role, and by the groups they are in. The permissions that govern this section itself are: | Permission | Lets a member | | ------------------------- | ------------------------------------------ | | `team-member:view-any` | Open a team's members page. | | `team-member:invite` | Send and withdraw invitations. | | `team-member:update-role` | Move a member to another role. | | `team-member:remove` | Remove a member from the team. | | `role:view-any` | See the team's roles on the roles page. | | `role:create` | Create a role in the team. | | `role:update` | Rename, describe and re-permission a role. | | `group:view-any` | See the team's groups on the groups page. | | `group:create` | Create a group in the team. | | `group:update` | Edit a group and choose its members. | | `group:delete` | Delete a group. | | `activity:read` | Read the team's activity log. | Renaming and deleting a team, and deleting a custom role someone else created, are the owner's alone. ## Related - [Your account](/account): the account an invitation creates and the ways to sign in. - [Accepting an invitation](/account/invitations): what the person you invite sees. --- # Members and Guests Source: https://alm-docs.labs.zuselab.dev/workspace/members-and-guests By the end of this page you will know how people get into a team, what the difference between a staff member and a guest is, how to change what a member may do, and how to take someone out again. Every person in Zuse Bifrost arrives through an invitation. There is no sign-up page: you invite an email address, the address receives a signed link, and following the link creates the account if there is none and adds the person to your team. A colleague joins as **staff**, with a role that applies across the team. A client stakeholder joins as a **guest** of one project, sees only what that project shares, and holds no team-wide permissions at all. ## Before you begin | You need | Why | | ---------------------------------------- | --------------------------------------------------------------------------------------------------------- | | To see members | The members page opens for the owner and for members whose role carries `team-member:view-any`. | | `team-member:invite` | To send or withdraw invitations. The owner always may. | | `team-member:update-role` | To move a member to another role. | | `team-member:remove` | To remove a member. | | A project in the team, to invite a guest | A guest is invited to one project, so the team needs at least one project before a guest can be asked in. | ## Where members live Open **Account Settings → Teams**, then choose **Members** from the row menu of a team. The page has two parts. **Members** lists the owner first, then every member by name: | Column | What it shows | | ---------- | ------------------------------------------------------------------------------------------------------ | | **Person** | Initials, name and email address; your own row is marked _(you)_. | | **Role** | _Owner_ for the person who created the team, otherwise the member's role name. | | Row menu | **Change role** and **Remove from team**, shown only for members, and only when your role allows them. | **Pending Invitations** lists what has been sent and not yet accepted: | Column | What it shows | | --------- | -------------------------------------------------------------------------- | | **Email** | The invited address, with the name you typed beneath it when you gave one. | | **Role** | The role offered; for a guest, the project they are invited to. | | **Sent** | How long ago, and who sent it. | | Action | **Withdraw**, when you may invite. | ## Invite a colleague [step] 1. Press **Invite Someone**. 2. Type the **Email** address. Capitalisation and surrounding spaces are ignored; the address is stored in lower case. 3. Pick the **Role** the person will hold. The list holds the team's roles, default and custom; it starts on _Developer_. 4. Optionally type their **Name** and **Job title**. Both pre-fill the page where they create their account; neither is required. 5. Press **Send Invitation**. The invitation appears under **Pending Invitations**, and an email with the acceptance link is on its way. The page refuses, with a message, when the address already belongs to the owner or a member of the team, when an invitation to it is already pending (withdraw that one to send another), and when the role is not one of the team's. ## Invite a client as a guest [step] 1. Press **Invite Someone** and type the client's **Email**. 2. Pick **Guest** as the **Role**. A **Project** field appears. 3. Choose the **Project** the client is invited to. Only the team's live projects are offered. 4. Add their **Name** and **Job title** if you know them; the title is recorded on their project membership as well. 5. Press **Send Invitation**. A guest invitation without a project is refused, as is a project that belongs to another team. When the guest accepts, two things happen at once: they join the team under the `guest` role, which carries no team permissions, and a project membership is written that marks them as external to the team and gives them the guest view of that one project. <Callout type="info"> A guest exists only on the projects they were invited to. They see the approved artefacts, the approvals that name them, their own tickets and the releases and incident status those projects share; estimates, time, internal comments, repositories and the other projects of the team stay hidden. </Callout> ## What the invitee receives The email is titled _You are invited to <team> on ZuseGenius_. It names the role and, for a guest, the project, and carries one button, **Accept the invitation**, whose link is signed: it works only as sent, and only until the invitation is accepted or withdrawn. [Accepting an invitation](/account/invitations) describes what the person sees next, which depends on whether they already hold an account. <Steps> <Step> **A new address** gets the only sign-up form in Zuse Bifrost: their name, pre-filled from your invitation, and a password. The account starts verified, because the signed link reached the inbox. </Step> <Step> **An existing account, signed out,** is sent to sign in; the sign-in completes the acceptance. </Step> <Step> **The invitee, already signed in,** joins the moment the link opens. </Step> </Steps> ## Withdraw an invitation [step] 1. Under **Pending Invitations**, press **Withdraw** on the row. 2. The row disappears and the link in that email stops working at once; whoever follows it is told the invitation is no longer valid. Nothing is announced for a withdrawal, because nobody joined. Send a fresh invitation if you change your mind. ## Change a member's role [step] 1. Open the member's row menu and choose **Change role**. The dialog opens on their current role. 2. Pick the new **Role** and press **Save**. 3. The new role applies the next time the member loads a page; nothing they have open is interrupted. Choosing the role they already hold saves and announces nothing. The owner has no role to change: ownership is not a role, and it stays with the person who created the team. ## Remove a member [step] 1. Open the member's row menu and choose **Remove from team**. 2. Press **Remove** in the confirmation. 3. They lose access to the team and its projects at once. If the team was their current one, they are pointed at another team of theirs, or at none. Their project memberships, comments and work stay as history. The owner cannot be removed. To take a guest off one project while leaving them in the team, use the project's own membership page when it lands; today a guest is removed from the team as a whole. ## All the fields <Accordions> <Accordion title="Email"> Required, a valid address. Normalised to lower case before it is compared with members and pending invitations, so `Client@Example.com` and `client@example.com` are the same invitation. </Accordion> <Accordion title="Role"> Required. One of the team's roles by code; the picker shows their names. Defaults to _Developer_, the most common invitation. Choosing _Guest_ reveals the **Project** field. </Accordion> <Accordion title="Project"> Required for a guest, absent for staff. One of the team's live projects; archived projects are not offered. Recorded on the invitation and turned into the guest's project membership when they accept. </Accordion> <Accordion title="Name"> Optional, up to 255 characters. Shown under the address in the pending list and pre-filled on the sign-up form, where the person can change it. </Accordion> <Accordion title="Job title"> Optional, up to 120 characters, such as _QA Lead_ or _Product Owner_. Copied to the account the invitation creates and, for a guest, to their project membership. </Accordion> </Accordions> ## What happens next | Action | Event announced | Payload | | -------------------- | ----------------------------------------------- | ----------------------------------------------------------------- | | Invitation sent | `team.member_invited` | The address, the invitation id, the role code and the team. | | Invitation accepted | `team.member_joined` | The role code, the team and the new member's user id. | | Guest accepted | `team.member_joined` and `project.member_added` | The project event marks the membership as external, role `guest`. | | Role changed | `team.member_role_changed` | The previous and the new role code. | | Member removed | `team.member_removed` | The team and the user id. | | Invitation withdrawn | nothing | Nobody joined, so there is nothing to announce. | ## Permissions | Action | Who may do it | | -------------------------- | ----------------------------------------------------- | | Open the members page | The owner, or a member with `team-member:view-any` | | Invite and withdraw | The owner, or a member with `team-member:invite` | | Change a member's role | The owner, or a member with `team-member:update-role` | | Remove a member | The owner, or a member with `team-member:remove` | | Change or remove the owner | Nobody | Buttons you are not allowed to use are not shown, and the action behind each one checks again when it runs. ## Frequently asked <Accordions> <Accordion title="The invitation email never arrived."> Ask the person to check their spam folder. If it is not there, withdraw the pending invitation and send a new one; each invitation is a fresh link. </Accordion> <Accordion title="Can one person be a guest of two projects?"> Yes. Invite them again for the second project once the first invitation is accepted: the team membership already exists, and the new invitation adds the second project membership on acceptance. </Accordion> <Accordion title="Can a guest be promoted to staff later?"> Yes. **Change role** moves them from _Guest_ to any staff role, and their team permissions follow at once. Their project memberships stay as they were. </Accordion> <Accordion title="Why does the Project field not appear?"> It appears only when the role picked is _Guest_. If it appears but is empty, the team has no live projects yet; create one first. </Accordion> <Accordion title="Can I resend the same invitation?"> Withdraw it and send a new one. The link is signed per invitation, so a new invitation is a new link. </Accordion> </Accordions> ## Related - [Teams](/workspace/teams): where the members page is opened from. - [Roles](/workspace/roles): what each role lets a member do. - [Accepting an invitation](/account/invitations): the page the link opens. --- # Roles Source: https://alm-docs.labs.zuselab.dev/workspace/roles By the end of this page you will know what the default roles are for, how to shape a role of your own from the permission catalogue, and why a role's code never changes. A role is a named set of permissions inside one team. Every member holds exactly one role per team; the owner holds none, because the owner may do everything. A permission is written as `entity:action`, such as `work-item:create` or `release:approve`, and a role is simply the list of permissions it grants. ## Before you begin | You need | Why | | --------------- | ------------------------------------------------------------------------------------------ | | `role:view-any` | To see a team's roles on the page. The owner always can. | | `role:create` | To create a role in a team. | | `role:update` | To rename, describe or re-permission a role. | | To own the team | To delete a custom role someone else created. The person who created it may delete it too. | ## The default roles Every team is seeded with these roles, and a project can mirror them with per-project overrides: | Role | Code | Intended for | | -------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- | | **Admin** | `admin` | Everything the owner may do inside the team, short of renaming or deleting it. | | **Project Manager** | `project_manager` | Running projects end to end: planning, approvals, sprints, releases and maintenance, without the workspace plumbing. | | **Business Analyst** | `business_analyst` | The planning artefacts in full (epics, stories, requirements, rules, use cases, personas, glossary), reading elsewhere. | | **Developer** | `developer` | The backlog, board, code links and time logs in full; planning read; tests read and execute; releases read. | | **QA Engineer** | `qa_engineer` | Test management and defects in full; planning read; releases read, with sign-off on acceptance runs. | | **Release Manager** | `release_manager` | Releases, deployments and environments in full, reading elsewhere. | | **Viewer** | `viewer` | Reads everything, changes nothing. | | **Guest** | `guest` | No team permissions at all. A guest's access comes from the project membership their invitation created. | The default roles can be re-permissioned to suit your team, but never deleted: the project roles are built on them. <Callout type="info"> Re-running the workspace seeder after the permission catalogue grows brings every team's default roles up to date without duplicating them; a custom role is left exactly as you made it. </Callout> ## Where roles live Open **Account Settings → Roles**. The page lists the roles of every team where your role lets you view them, by name: | Column | What it shows | | --------------- | -------------------------------------------------------------------------------------------------------- | | **Role** | The name, the code in small type, a **Default** badge on a seeded role, and the description beneath. | | **Team** | Which team the role belongs to, since the page spans your teams. | | **Permissions** | How many permissions the role carries. | | Row menu | **Edit** when you may update roles in that team, **Delete** when the role is custom and yours to delete. | ## Create a role [step] 1. Press **Create a Role**. The button appears when at least one of your teams lets you create roles. 2. Pick the **Team** the role belongs to. 3. Type a **Name**, a **Code** and, if useful, a **Description**. 4. Tick the **Permissions** the role should grant. They are grouped by entity, one group per kind of thing, with the five common actions and the named actions each entity has. 5. Press **Create**. The code must be unique within the team; a second role with the same code is refused with a message. Once created, the code is fixed. ## Edit a role [step] 1. Open the role's row menu and choose **Edit**. The flyout opens with the code shown but not editable. 2. Change the **Name**, the **Description** or the **Permissions**, and press **Save**. 3. Members who hold the role gain or lose the permissions the next time they load a page. Saving without changing anything writes and announces nothing. ## Delete a role [step] 1. Open the row menu of a custom role and choose **Delete**. 2. Press **Delete Role** in the confirmation. 3. Members who held the role keep their membership but lose the permissions it granted; move them to another role from the [members page](/workspace/members-and-guests). A default role cannot be deleted. A custom role can be deleted by the person who created it or by the team's owner, and by nobody else: belonging to a team is not a licence to dismantle how everyone else in it is permissioned. ## The permission catalogue Permissions follow one grammar, `entity:action`. Most entities carry the five common actions: | Action | Lets a member | | ---------- | ----------------------------- | | `view-any` | Open the list of that entity. | | `view` | Open one item. | | `create` | Add one. | | `update` | Change one. | | `delete` | Remove one. | Some entities add named actions for the moments that matter: `epic:approve`, `work-item:transition`, `sprint:start`, `release:publish`, `incident:command`, `team-member:invite`, and so on. The editor shows the whole catalogue, including entities whose pages are still to land, so a role you shape today keeps meaning the same thing as the product grows. ## All the fields <Accordions> <Accordion title="Team"> Required when creating. One of the teams where you may create roles. A role belongs to one team; the same code in two teams is two roles. </Accordion> <Accordion title="Name"> Required, up to 255 characters. What members and pickers show. </Accordion> <Accordion title="Code"> Required when creating, up to 64 characters of letters, numbers, dashes and underscores, unique within the team. Fixed afterwards: permissions, invitations and the API address a role by its code, and renaming one would silently detach every assignment. </Accordion> <Accordion title="Description"> Optional, up to 255 characters. Shown under the name in the list. </Accordion> <Accordion title="Permissions"> Any number of codes from the catalogue; an unknown code is refused. An empty set is allowed and makes a role that can see nothing beyond what the team shows every member. </Accordion> </Accordions> ## What happens next | Action | Event announced | Payload | | ------------- | --------------- | ------------------------------------------------------------------------------------------- | | Create a role | `role.created` | The code, the name, the full permission list, the role id and the team. | | Edit a role | `role.updated` | Field-by-field changes: the name, the description and the permission sets before and after. | | Delete a role | `role.deleted` | The code, the name, the role id and the team. | ## Permissions | Action | Who may do it | | --------------------- | ------------------------------------------- | | See a team's roles | The owner, or a member with `role:view-any` | | Create a role | The owner, or a member with `role:create` | | Edit a role | The owner, or a member with `role:update` | | Delete a custom role | The owner, or the member who created it | | Delete a default role | Nobody | ## Frequently asked <Accordions> <Accordion title="I made a typo in a role code. How do I fix it?"> Create a new role with the right code and the same permissions, move the members over, then delete the old one. The code is fixed on purpose. </Accordion> <Accordion title="Can a member hold two roles in one team?"> No. One role per team per member. Use a [group](/workspace/groups) to grant extra permissions to several members without changing their roles. </Accordion> <Accordion title="Why can I see a role but not edit it?"> Seeing roles needs `role:view-any`; editing needs `role:update`. The owner holds both, and a default role such as _Viewer_ holds only the first. </Accordion> <Accordion title="What does a guest's role grant?"> Nothing at the team level. A guest's access is the guest view of the project their invitation named. Giving the `guest` role permissions would turn every guest into staff, so leave it empty. </Accordion> </Accordions> ## Related - [Members and guests](/workspace/members-and-guests): assign a role when inviting, or change one later. - [Groups](/workspace/groups): permissions granted on top of a role. - [Teams](/workspace/teams): roles belong to a team. --- # Teams Source: https://alm-docs.labs.zuselab.dev/workspace/teams By the end of this page you will know what a team is for, how to move between the teams you belong to, and what creating, renaming and deleting a team does to the people and projects in it. A team is the unit that owns projects. Each team has one owner, the person who created it, and any number of members, each with a role. Everything you open in Zuse Bifrost acts within one team at a time: the team marked **Current**. ## Before you begin | You need | Why | | ------------------------------------ | ---------------------------------------------------------------------------------------------------- | | An account | Anyone signed in may create a team and becomes its owner. | | To own the team | Renaming and deleting a team are the owner's alone; a member with the `admin` role cannot do either. | | Another team, to delete your current | You cannot delete your only team, because every page acts within one. | ## Where teams live Open the account menu at the bottom of the sidebar and choose **Account Settings**, then **Teams** in the settings navigation. The page lists every team you own or belong to, alphabetically: | Column | What it shows | | ------------- | ------------------------------------------------------------------------------ | | **Team** | The team's name, with a **Current** badge on the one your pages act within. | | **Your role** | _Owner_ for a team you created, otherwise the name of the role you hold in it. | | **Members** | How many people are in the team, the owner included. | The account menu also carries the **team switcher**: the same teams as a list of radio items, with a **Manage Teams** link to this page. ## Create a team [step] 1. On the **Teams** page, press **Create a Team**. 2. Type the **Team name** and press **Create**. 3. The team appears in the table with you as its owner. If you had no current team yet, the new one becomes current at once, so your first team is usable the moment it exists. A new team is seeded with the eight [default roles](/workspace/roles), so you can invite people straight away. ## Switch to another team [step] 1. Open the row menu (the three dots) of the team you want to work in and choose **Switch to this team**, or pick the team in the account menu's switcher. 2. The dashboard reloads within that team. Everything you open from now on, projects included, belongs to it. Switching is navigation, not a change to the team or your membership; nothing is logged or announced. ## Open a team's members [step] 1. Open the team's row menu and choose **Members**. 2. The [members page](/workspace/members-and-guests) opens, if your role in that team lets you see members. The owner always can. ## Rename a team [step] 1. Open the row menu of a team you own and choose **Rename**. 2. Change the **Team name** and press **Save**. 3. The new name shows everywhere at once, and a `team.updated` event carries the old and new name to any webhook subscribed to it. Saving the same name again writes and announces nothing. ## Delete a team [step] <Callout type="warn"> Deletion is immediate and cannot be undone. Its members lose access at once, and the team's roles, groups and pending invitations are removed with it. </Callout> 1. Open the row menu of a team you own and choose **Delete**. 2. Read the confirmation, which names the team, and press **Delete Team**. 3. Every member who was working in that team is left without a current team and picks one on their next visit. If the deleted team was your own current team, you are moved to another team of yours, an owned one first. Deletion is refused, with a message instead of a dialog, when: - the team is your only one; - the team still owns projects, archived ones included. Move or delete them first, so no project is left without a team; - you are not the owner. ## All the fields <Accordions> <Accordion title="Team name"> Required, between 2 and 255 characters. Shown in the switcher, in the settings navigation of every member, on invitations and in webhook payloads. Names need not be unique; the team's id is what the system uses. </Accordion> </Accordions> ## What happens next | Action | Event announced | What else changes | | ------------- | --------------- | ----------------------------------------------------------------------------------------- | | Create a team | `team.created` | The default roles are seeded; the creator becomes owner and, if they had none, current. | | Rename a team | `team.updated` | The payload lists the change as `name: from → to`. | | Delete a team | `team.deleted` | Memberships, roles, groups and pending invitations go; current-team pointers are cleared. | | Switch team | nothing | Only your own current-team pointer changes. | ## Permissions | Action | Who may do it | | ------------------------ | -------------------------------------------------- | | See the team in the list | The owner and every member | | Create a team | Anyone signed in | | Switch to a team | The owner and every member of that team | | Open the members page | The owner, or a member with `team-member:view-any` | | Rename or delete a team | The owner only | ## Frequently asked <Accordions> <Accordion title="Can I hand a team to someone else?"> Not yet. The person who created a team owns it. Invite the new owner as an `admin` so they can run everything but renaming and deletion, and plan the hand-over for when ownership transfer lands. </Accordion> <Accordion title="Why is my Delete menu item missing?"> Only the owner sees **Rename** and **Delete**. If you own the team and the delete is refused, the message tells you whether it is your only team or still owns projects. </Accordion> <Accordion title="What do members see when I delete a team they were working in?"> Their next page load has no current team. They pick another team from the switcher or the Teams page; if they belonged to no other team, they see an empty workspace until they are invited somewhere. </Accordion> <Accordion title="Does leaving a team exist?"> A member is removed by someone with the remove permission on the members page. Removing yourself from a team you do not own is not offered yet. </Accordion> </Accordions> ## Related - [Members and guests](/workspace/members-and-guests): who is in a team and how they got there. - [Roles](/workspace/roles): what each member may do. - [Accepting an invitation](/account/invitations): the invitee's side. --- # Webhooks Source: https://alm-docs.labs.zuselab.dev/workspace/webhooks By the end of this page you will know how to register a URL that Zuse Bifrost posts events to, what the events look like at a glance, how to keep the signing secret safe, and how to read the delivery log when a consumer misbehaves. A webhook endpoint is a URL of yours that receives a signed POST every time a subscribed event happens in your team: a story approved, a release promoted, an incident opened. It is how pipelines, chat alerts and bespoke automations learn about work without polling. The endpoint belongs to the team you are working in; everyone in the team sees it, and its creator and the team owner manage it. ## Before you begin | You need | Why | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | A public https URL | The target must be reachable from the internet over https. Private addresses and plain http are refused. | | To be in a team | Endpoints are filed under the team you are working in; switch teams first if you mean another. | | A consumer that verifies signatures | Every delivery carries a `ZG-Signature` header. [Signature verification](/webhooks/v1/signature-verification) shows the check. | ## Where webhooks live Open **Account Settings → Webhooks**. The page lists the team's endpoints, newest first: | Column | What it shows | | ------------ | --------------------------------------------------------------------------------- | | **Endpoint** | The name and the URL. | | **Events** | How many events it subscribes to. | | **Status** | _Active_, or _Disabled_ when paused by a person or after too many failures. | | Row menu | **Send test delivery**, **Rotate secret**, **Disable** or **Enable**, **Delete**. | The **Deliveries** button opens the [delivery log](#read-the-delivery-log). ## Create an endpoint [step] 1. Press **Create Endpoint**. 2. Type a **Name** that says who listens, such as _Deploy pipeline listener_. 3. Type the **URL**. It must be https, and its host must resolve to a public address. 4. Tick the **Events** to subscribe to. They are grouped by family (teams, roles, tokens, projects, and every family that lands with the phases to come), with a **Toggle** per family and one for everything. 5. Press **Create endpoint**. A flyout shows the signing **secret** once, with a copy button. Store it in your consumer's secret store before pressing **Done**; it is kept encrypted afterwards and never shown again. Verify every delivery against it, as [signature verification](/webhooks/v1/signature-verification) describes. <Callout type="info"> Choose events, not families, when you can: a consumer that handles only `release.released` should subscribe to that alone, so a change elsewhere never reaches it as noise. </Callout> ## Send a test delivery [step] 1. Open the endpoint's row menu and choose **Send test delivery**. 2. A synthetic `project.created` payload is queued, marked `synthetic: true` in its data, and posted with a real signature. It appears in the delivery log like any other delivery, so you can see what your consumer answered. ## Rotate the secret [step] 1. Open the row menu and choose **Rotate secret**, then confirm. 2. The new secret is shown once in the flyout. Update your consumer; deliveries signed with the old secret stop verifying from the next delivery on. Rotate when a secret may have been seen by someone who should not hold it, and on a schedule of your own choosing. ## Pause and resume [step] 1. Choose **Disable** to stop deliveries to an endpoint without deleting it. Events that fire while it is paused are not queued for it. 2. Choose **Enable** to resume. Re-enabling clears the endpoint's failure count, so an endpoint that was switched off after an outage starts with a clean slate. An endpoint is also disabled on its own after a long run of consecutive failures, so a dead URL does not keep a retry ladder busy forever. The status column says so, and **Enable** brings it back once the consumer is fixed. ## Delete an endpoint [step] 1. Open the row menu and choose **Delete**, then confirm. 2. The endpoint and its delivery history are removed, and any retries still pending for it are dropped. ## Read the delivery log Press **Deliveries** on the webhooks page, or open **Account Settings → Webhooks → Deliveries**. The log lists every delivery the team's endpoints were sent, newest first, with tabs for **All**, **Pending**, **Delivered**, **Failed** and **Dead**; the tab is in the address, so a link to the dead-letter view survives a reload. | Column | What it shows | | ------------ | --------------------------------------------------------------------------------------------------------------------- | | **Delivery** | The endpoint's name (or _deleted endpoint_) and the event name. | | **Status** | Pending, delivered, failed (will be retried) or dead (retries exhausted), with the HTTP status the consumer answered. | | **Attempts** | How many times it has been posted. | | **Created** | How long ago the event fired. | | Row menu | **Details**, and **Retry delivery** on failed and dead rows. | **Details** opens a flyout with the event, the status, the attempts, the HTTP status, when it was created and when the next attempt is due, the excerpt of the consumer's response body, and the full payload as JSON. That excerpt is usually the fastest way to see why a consumer rejected a delivery. ## Replay deliveries [step] 1. For one delivery, open its row menu and choose **Retry delivery**. The row goes back to the start of the retry ladder and is posted again shortly. 2. For everything dead-lettered in the last day, press **Replay dead (24h)** and confirm. The toast says how many were queued. Older dead rows stay as they are; replay them one by one if they still matter. Only failed and dead deliveries can be retried; a delivered one is final. ## All the fields <Accordions> <Accordion title="Name"> Required, 3 to 100 characters. Shown in the list and in the delivery log. </Accordion> <Accordion title="URL"> Required, up to 2,048 characters, https only, and the host must resolve to a public address: a hostname that points at a private or loopback address is refused, so a webhook can never be aimed at an internal service. Stored encrypted. </Accordion> <Accordion title="Events"> At least one, from the event catalogue. Unknown names are dropped, and an endpoint left with none is refused. The webhooks section lists every event with its payload. </Accordion> </Accordions> ## What happens next Registering, pausing, rotating or deleting an endpoint announces nothing to other endpoints; the delivery log is the record. Every delivery is retried with exponential back-off before it is dead-lettered, as [retries and delivery](/webhooks/v1/retries-and-delivery) describes, and the same page explains what the status codes your consumer answers mean to the ladder. ## Permissions | Action | Who may do it | | --------------------------------------- | ----------------------------------------------------------- | | See the team's endpoints and deliveries | Every member of the team | | Create an endpoint | Every member of the team, filed under the team they work in | | Test, rotate, pause, resume, delete | The endpoint's creator and the team owner | | Retry and replay deliveries | Every member of the team | ## Frequently asked <Accordions> <Accordion title="Why was my URL refused?"> It is plain http, or its host resolves to a private, loopback or link-local address. Use an https URL on a public host; for local development, a tunnel that gives you a public hostname works. </Accordion> <Accordion title="Why did the endpoint switch itself off?"> Too many consecutive deliveries failed. Fix the consumer, press **Enable**, and replay what was dead-lettered meanwhile. </Accordion> <Accordion title="Can I scope an endpoint to one project?"> The API accepts a project when registering an endpoint, and the dashboard form gains the picker with the project pages. Until then, an endpoint registered here receives the team's events for every project. </Accordion> <Accordion title="How do I verify the signature?"> [Signature verification](/webhooks/v1/signature-verification) walks through the `ZG-Signature` header with code in several languages, and [testing and debugging](/webhooks/v1/testing-and-debugging) shows how to use the test delivery while you build the consumer. </Accordion> </Accordions> ## Related - [Webhooks quickstart](/webhooks/v1/quickstart): a first consumer end to end. - [Security](/webhooks/v1/security): secrets, allow-lists and what a consumer should check. - [API tokens](/workspace/api-tokens): the credential for the other direction. ---