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: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:Request sandbox keys and wire the SDK behind a feature flag.
", "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": "Customers reset their password and see their orders.
", "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": "Tell a member they reached the next tier the day it happens.
", "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": "Customers reset their password and see their orders.
", "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": "Tell a member they reached the next tier the day it happens.
", "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": "One customer's laundry order, from placement to collection.
", "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": "A customer leaving laundry at the shop counter rather than booking a pickup.
", "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": "One purchase a branch places, from the basket to the delivery note.
", "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": "One physical shop of the client, with its own stock, staff and opening hours.
", "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": "One purchase a branch places, from the basket to the delivery note.
", "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": "One physical shop of the client, with its own stock, staff and opening hours.
", "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": "One customer's laundry order, from placement to collection.
", "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": "A customer leaving laundry at the shop counter rather than booking a pickup.
", "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": "Book a pickup in under a minute and know when the laundry is ready.
", "frustrations": "Calling the shop to ask whether the order is done.
", "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": "Mid-forties, twelve years at the company, works from the regional office.
", "goals": "Works on a laptop at the desk and a phone on the warehouse floor.
", "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": "Prefers a call over email for anything urgent.
", "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": "Mid-forties, twelve years at the company, works from the regional office.
", "goals": "Works on a laptop at the desk and a phone on the warehouse floor.
", "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": "Prefers a call over email for anything urgent.
", "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": "30 to 45, two children, both parents working.
", "goals": "Book a pickup in under a minute and know when the laundry is ready.
", "frustrations": "Calling the shop to ask whether the order is done.
", "behaviours": "Orders from the phone in the evening; pays by card.
", "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": "The client wants the voucher applied to the first renewal as well.
", "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": "CRM-FR-12 gains a renewal clause; the story that implements it and its tests change.
", "impact_schedule_days": 3, "impact_effort_hours": "12.50", "impact_cost": null, "impact_risk": "The renewal job runs unattended; a wrong template confuses rather than leaks.
", "impact_level": "medium", "ai_impact_draft": "A small change to the renewal flow; the story that implements the requirement changes with it.
CRM-FR-12 gains a renewal clause; the story that implements it and its tests change.
The renewal job runs unattended; a wrong template confuses rather than leaks.
Confirmed by CRM-BR-7: the voucher comes last.
", "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": "The second release, as agreed on the October call.
", "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": "A customer picks services and items and chooses pickup or drop-off.
", "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": "The payment provider has not issued sandbox keys; the checkout cannot be tested end to end.
", "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": "The system shall meet WCAG 2.2 level AA on every screen a member can reach.
", "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": "The order is paid.
", "postconditions": "A pickup slot is held and the driver's route includes the address.
", "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": "Everything the client signed for the first release.
", "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": "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", "exceptions": "Spend refunded within thirty days does not count.
", "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": "The client wants the voucher applied to the first renewal as well.
", "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": "CRM-FR-12 gains a renewal clause; CRM-142 and its checkout tests change.
", "impact_schedule_days": 5, "impact_effort_hours": "24.00", "impact_cost": "1800.00", "impact_risk": "The renewal job runs unattended; a voucher applied twice would refund twice.
", "impact_level": "medium", "ai_impact_draft": "A small change to the renewal flow; the story that implements the requirement changes with it.
…
", "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": "Voucher codes are never written to logs or analytics in clear.
", "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": "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, 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": "The ledger lives in Postgres, partitioned by month from the first release.
", "consequences": "The reporting queries join the ledger directly.
", "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": "Members climb silver, gold and platinum tiers by annual spend.
", "goal": "Reward the top fifth of members so they keep buying from the client.", "business_value": "Retention of the top tier is worth more than every acquisition campaign combined.
", "hypothesis": "A visible next tier raises repeat purchases within a quarter.", "success_metrics": "Repeat purchase rate of gold members up ten points by the second quarter after launch.
", "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": "The provider ships new card flows to production weeks before its sandbox.
", "category": "external", "probability": 3, "impact": 4, "score": 12, "mitigation": "Run the checkout smoke test against the provider's production account every Monday.
", "contingency": "Pin the gateway to the last verified flow and raise an incident with the provider.
", "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": "The system shall let a member reset their password from the sign-in page by email.
", "rationale": "Support spends an hour a day resetting passwords by hand.
", "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": "The member is signed in and the basket is not empty.
", "postconditions": "The voucher is marked used and the order carries the discount.
", "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": "Everything the client signed for the first release.
", "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": "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", "exceptions": "Spend refunded within thirty days does not count.
", "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": "The client wants the voucher applied to the first renewal as well.
", "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": "CRM-FR-12 gains a renewal clause; CRM-142 and its checkout tests change.
", "impact_schedule_days": 5, "impact_effort_hours": "24.00", "impact_cost": "1800.00", "impact_risk": "The renewal job runs unattended; a voucher applied twice would refund twice.
", "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": "Does the voucher apply before or after the loyalty discount? @Sam Lee
", "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": "After it: CRM-BR-7 says the voucher comes last.
", "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": "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, 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": "The ledger lives in Postgres, partitioned by month from the first release.
", "consequences": "The reporting queries join the ledger directly.
", "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": "Members climb silver, gold and platinum tiers by annual spend.
", "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": "The provider ships new card flows to production weeks before its sandbox.
", "category": "external", "probability": 3, "impact": 4, "score": 12, "mitigation": "Run the checkout smoke test against the provider's production account every Monday.
", "contingency": "Pin the gateway to the last verified flow and raise an incident with the provider.
", "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": "The system shall render a member's tier page within 300 ms at the 95th percentile with 500 members browsing at once.
", "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": "The member is signed in and the basket is not empty.
", "postconditions": "The voucher is marked used and the order carries the discount.
", "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": "A member may return an order for a refund within thirty days of delivery.
", "condition": "When a return is requested", "action": "then it is accepted only if fewer than thirty days have passed since delivery", "exceptions": "Perishable goods are not returnable.
", "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": "The client wants the voucher applied to the first renewal as well.
", "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": "The provider ships new card flows to production weeks before its sandbox.
", "category": "external", "probability": 3, "impact": 4, "score": 12, "mitigation": "Run the checkout smoke test against the provider's production account every Monday.
", "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": "The digests must not block the web requests.
", "options": [ { "title": "Redis queues", "pros": "Already in the stack", "cons": null }, { "title": "A managed queue service", "pros": null, "cons": "Another vendor" } ], "decision": "Redis queues, one worker per project.
", "consequences": "Each project gets its own queue worker.
", "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": "The pilot cut of the second release.
", "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": "A pickup slot can be booked only against an order that is paid in full.
", "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": "The client wants the voucher applied to every renewal.
", "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": "CRM-FR-12 gains a renewal clause; CRM-BR-7 gains an exception for renewals.
", "impact_schedule_days": 5, "impact_effort_hours": "24.00", "impact_cost": "1800.00", "impact_risk": "The renewal job runs unattended.
", "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": "The shop wants card payments at placement; two gateways serve Sri Lanka.
", "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": "The house gateway, with the sandbox first.
", "consequences": "Fees are passed on above a threshold; the bank gateway is revisited at volume.
", "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": "Services, items, pickup or drop-off, and the payment step.
", "goal": "A customer books a wash in under two minutes.", "business_value": "Every order placed online is one the counter does not type in.
", "hypothesis": null, "success_metrics": "Seventy percent of orders placed online by month three.
", "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": "Approved on the second application; keys arrived on the 3rd.
", "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": "The system shall take a card payment at order placement.
", "rationale": "The counter stops chasing payment on collection.
", "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": "The order is paid.
", "postconditions": "A pickup slot is held and the driver's route includes the address.
", "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": "The customer-facing site: services, ordering, tracking and bookings.
", "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": "The three screens of placing an order; the payment step is a placeholder.
", "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": "An informational website with laundry order placement, tracking, payment and appointment scheduling.
", "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": "Prefers a weekly status call on Tuesdays.
", "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": "The portal account holders sign in to.
", "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": "Frames 12 to 18 are the signed-off flow; the rest are explorations.
", "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": "Account holders manage invoices, tickets and contacts without calling support.
", "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": "The portal account holders sign in to.
", "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": "Frames 12 to 18 are the signed-off flow; the rest are explorations.
", "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": "Resets its database every Sunday night.
", "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": "The customer-facing site: services, ordering, tracking and bookings.
", "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": "The payment step is now designed; the slot picker moved to its own screen.
", "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": "An informational website with laundry order placement, tracking, payment and appointment scheduling.
", "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.