Blueprints
- Define a custom type on a repository
- Define a custom type for every repository in a group
- See which types a repository offers
- See the types a group defines
- Read one type's definition
- File work as a custom type
- Apply a type to work already filed
- Change a type
- Retire a type without breaking anything
- Delete a type
Overview
Every team has a shape for a kind of work that never reaches the tracker: a new endpoint also needs a metric, a test, an access rule and a page of documentation, and the person who knows that is the reviewer who says so afterwards. A blueprint writes that shape down as a custom work-item type — a display name, one of the eight base types, and a list of facets the work has to cover. File an item as that type and the facets land on it as acceptance criteria, ticked automatically wherever a pipeline can prove them. Define the type once on a group and every repository beneath it offers it.
Quick start
Give a kind of issue its own Definition of Done
Start at Work item types in the side navigation.
Start from one of the templates — or design your own type.
Give each facet a label. Every facet becomes an acceptance criterion on the issues filed as this type.
Choose Create type. It appears in the type picker on New issue.
Before you start
- Maintainer access on the repository or group you define the type on.A type is shared vocabulary that reaches every item filed against it, so defining one sits at the same level as minting labels and milestones.
- Reporter access to read the types a container offers.Reading follows the container's visibility: a public repository answers anyone, an internal one answers anyone signed in, and a private one answers only its members. A private container answers
404rather than403, so its existence stays private. - Developer access on the repository to apply a type to work already filed.Applying writes acceptance criteria onto the item, which is the same bar as authoring one by hand.
- Reporter access to file work as a custom type.Picking the type on the new-item form is the whole of it, so filing as a custom type costs no more access than filing anything else.
- A token with the right scopes, if you are scripting:
issue_blueprint:readto list and read types,issue_blueprint:manageto define, change, archive or delete one, andissue:updateto apply one to an item. A token carries the scopes you chose when you minted it, and they cannot be added to it afterwards. - A decision about where the type lives.A type defined on a group is offered by every repository beneath it; a type defined on a repository is offered only there. If two repositories file the same kind of work, define it once on the group above them.
- Acceptance criteria, if you want facets that prove themselves.A facet is a named group of criteria, and a bound facet is decided by the evidence your pipelines already emit — see Proof of Done for what can be bound and how the evidence gets there.
How it works
A custom type is a base type wearing a name
A blueprint does not invent a new kind of object. It picks one of the eight base types — epic, story, bug, task, spike, chore, incident, sub_task — and adds two things to it: a display name and a facet list. "API Feature" is a story wearing a blueprint. The base type still decides everything structural: which container the item may live in, which children it may hold, and which fields it has. Nothing about hierarchy, boards, listings or rollups changes because an item was filed as a custom type.
That is also why one rule exists that surprises people: an epic lives only in a group, and a group has no pipelines to prove anything, so an epic-based blueprint may declare manual facets only. A bound facet on one is refused at the moment you save the type, not silently dropped when somebody files an item.
Facets become acceptance criteria
Applying a blueprint seeds one acceptance criterion per facet. There is no second checklist and no generated markdown — the facets are the item's Definition of Done, written as the same typed criteria you would author by hand.
Blueprint "API Feature" defined on the group acme base type: story facets: endpoint category: interface required manual tests category: quality required bound to a test result observability category: operability required manual docs category: docs optional manual | file an item as "API Feature", or apply it to one already filed v Work item acme/billing #412 "Add the refund endpoint" type: story — the blueprint's base type Definition of Done Endpoint you tick it Tests the pipeline decides it — you cannot tick it by hand Observability you tick it Docs optional, so it does not hold the item openEach facet carries a category — interface, operability, accessibility, runbook, whatever your work decomposes into. The vocabulary is yours: a category the platform has never seen is a first-class category, stored and grouped exactly like the ones it has. A facet is green when every one of its required criteria is satisfied, and an item is verified when every required criterion on it is green — which is exactly what the merge gate reads in a repository that has turned it on.
A facet's hint becomes the criterion's text. Leave it out and the criterion is titled by the facet's label, which is fine for Tests and useless for Observability — a hint like "the endpoint emits a request metric and an error metric" is the difference between a checklist and a checklist somebody can act on.
The key is the identity, the label is just the label
Every facet has a key — a slug of 1 to 40 characters, lowercase letters, digits and hyphens — and that key is what the platform matches on. Rename the "Observability" facet to "Operability" and keep its key, and an item that already carries it keeps the criterion it has, with its tick and its history. Change the key instead and you have made a different facet: the old one stays on every item that has it, and the new one arrives beside it the next time the type is applied.
Types are inherited down the group chain
The types a container offers are its own plus every ancestor group's. Ask a repository for its available types and you get its own first, then the inherited ones nearest group first. Where two definitions share an id, the nearest one wins — a repository Maintainer can define api-feature locally and shadow the organisation's version, without affecting any other repository and without needing anything from the group.
Inheritance runs upward only. A type defined on a repository is invisible to its group, and a type defined on a sibling group is invisible here.
Applying is additive, and never a clobber
Applying a blueprint to an item compares facet keys and seeds only the ones the item does not already have. It never rewrites, reorders, unticks or removes an acceptance criterion: an existing facet keeps its tick, its evidence and any criteria a person added underneath it. So re-applying a type after it grew a fifth facet adds one criterion and disturbs nothing.
The consequences of that rule are worth stating plainly, because they are what make the feature safe to use on live work:
- Editing a blueprint does not reach back into items already filed. Adding a facet does not appear on last month's stories until somebody re-applies the type to them.
- Removing a facet from a blueprint does not retract it from anything. Items that were filed with it keep it.
- Deleting a blueprint leaves every seeded criterion intact. A tracker that silently deleted acceptance criteria out from under a team would be a tracker nobody could trust with a Definition of Done.
What an item takes from a blueprint is its Definition of Done, and only that. The item's own type stays the base type the blueprint named — an item shaped by "API Feature" is a story, and reads as a story everywhere a type is shown.
A type is addressed by the container that defines it
A blueprint is named by the container it was defined on, never by the container a work item happens to sit in. That is what makes an inherited type usable — a repository's item can be filed as a type defined three groups above it — and it is why, whenever you name a type outside the picker, you give the group or repository that owns the definition. Every listing entry carries that container, so it is the place to read the id from rather than guessing.
Filing with a type and applying a type are not quite the same
Filing a new item as a custom type sets the item's underlying type from the blueprint's base type and seeds the facets in the same write. It also checks the base type against the container: an epic-based type files in a group, an incident-based one in either, every other base type files in a repository, and the wrong pairing is refused with 400 BAD_REQUEST before anything is written. Applying a type to an item that already exists seeds the facets it is missing and leaves the underlying type where it was — so a task that gets a story-based type is still a task, with a task's fields and a task's legal children. When you want the structure too, file the item as the type; when you want the checklist on work already in flight, apply it.
How to
Define a custom type on a repository
Give it an id, a display name, a base type and its facets. The id is the identity — 1 to 40 characters, lowercase letters, digits and hyphens, not starting with a hyphen — and it has to be free within that container. The display name is what people pick from the type selector.
Open the repository, choose Issues in the side navigation, then Types, then New type. The header of the builder takes the name, the base type and a description that shows as help text under the type in the picker. Below it, each row is a facet: a label, a category of your own naming (any lowercase slug, not only the ones the platform has seen before), a Required toggle that starts on, an optional hint, and a binding picker that starts at Manual. Drag rows to reorder them; the order is the order the criteria appear on the item. Save type returns you to the list.
gitmoap issue-blueprints create-repo-blueprint \ --repo-id <id> \ --base-type epic|story|bug|task|spike|chore|incident|sub_task \ --blueprint-id <blueprint id> \ --name <name>curl -X POST https://api.gitmoap.example/repositories/{repo_id}/issue-blueprints \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "base_type": "<one of bug | chore | epic | incident | spike | story | sub_task | task>", "blueprint_id": "<string>", "facets[].category": "<string>", "facets[].key": "<string>", "facets[].label": "<string>", "name": "<string>" }'Every facet is a manual checkbox until you bind it, and that is the right default — most facets on most teams are not worth binding, and a type that demanded machine-checkable everything would go unused. Bind the ones that pay: a test result, a metric threshold, a coverage floor over a path, or a pipeline node going green. A bound facet's box is filled in from evidence and cannot be ticked by a person, which is the whole point of binding it.
Define a custom type for every repository in a group
Same builder, one level up. This is where a type belongs if more than one repository files that kind of work.
Open the group, choose Issues then Types, then New type. The base-type selector here offers the group-legal types as well — Epic and Incident can be filed in a group — and a type based on any of the repository types is still defined here and offered to every repository beneath.
gitmoap issue-blueprints create-group-blueprint \ --group-id <id> \ --base-type epic|story|bug|task|spike|chore|incident|sub_task \ --blueprint-id <blueprint id> \ --name <name>curl -X POST https://api.gitmoap.example/groups/{group_id}/issue-blueprints \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "base_type": "<one of bug | chore | epic | incident | spike | story | sub_task | task>", "blueprint_id": "<string>", "facets[].category": "<string>", "facets[].key": "<string>", "facets[].label": "<string>", "name": "<string>" }'A type whose base type is epic may declare manual facets only, because an epic lives in a group and no group has pipelines that could decide a bound one. That is a property of the base type rather than of where you define it, so it holds for an epic-based type defined on a repository too. Trying to save a bound facet on one is refused with 400 BAD_REQUEST naming the facet. incident is not caught by this — an incident can live in a repository, so an incident-based type may bind.
See which types a repository offers
By default a repository reports only the types defined on it. Ask for inherited ones as well and you get the full picker: its own first, then each ancestor group's, nearest first, with any shadowed duplicate already resolved in favour of the nearer one. Each entry says whether it was inherited, and which container defines it.
Archived types come back in that listing too, each carrying its archived flag. The listing is the raw available set; leaving the archived ones out of the picker is the picker's job, so a script that reads this should filter them itself.
Open the repository, choose Issues then Types. The list shows the repository's own types and the ones it inherits, each tagged with the container it comes from, and archived ones greyed. A filter on the list narrows it to the types defined here.
gitmoap issue-blueprints list-repo-blueprints --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/issue-blueprints \ -H "Authorization: Bearer $GITMOAP_TOKEN"See the types a group defines
Open the group, choose Issues then Types. This is the list every repository beneath the group inherits from, so it is the one to edit when you want a change to reach the whole organisation.
gitmoap issue-blueprints list-group-blueprints --group-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/issue-blueprints \ -H "Authorization: Bearer $GITMOAP_TOKEN"Read one type's definition
Reading one type asks the container that defines it. A repository will not answer for a type it merely inherits — that read belongs to the group that owns it, and asking the repository gives 404 NOT_FOUND.
Open Issues then Types and choose a row defined on this repository. The detail view shows the base type, the description and every facet with its category, its required flag and its binding.
gitmoap issue-blueprints get-repo-blueprint \ --repo-id <id> \ --blueprint-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/issue-blueprints/{blueprint_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Open the group's Issues then Types and choose a row. A repository that inherits this type links here rather than showing a local copy, so there is one place it is edited.
gitmoap issue-blueprints get-group-blueprint \ --group-id <id> \ --blueprint-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/issue-blueprints/{blueprint_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"File work as a custom type
There is no separate "assign a blueprint" step, because picking the type is the assignment. The type selector on the new-item form lists the base types that container takes plus every custom type it offers, and choosing one files the item on the blueprint's base type with its facets already seeded, in a single write — if any part of it is refused, nothing is created. That path is covered in Work items.
Apply a type to work already filed
Applying seeds the facets the item is missing as acceptance criteria. Name the blueprint by the container that defines it — the group, if the type is inherited — and the item by its number in the repository.
Open the work item and use Apply type in the Definition of Done panel. The picker lists the same types the new-item form offers. The panel then shows the facets it added; anything already on the item is left exactly as it was, and the panel says so rather than pretending it did more.
gitmoap issue-blueprints apply-blueprint \ --repo-id <id> \ --issue-id <id> \ --blueprint '<json>'curl -X POST https://api.gitmoap.example/repositories/{repo_id}/issues/{issue_id}/apply-blueprint \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "blueprint.blueprint_id": "<string>", "blueprint.scope": "<object>" }'Applying the same type twice is safe and adds nothing the second time. Applying it after the type grew a facet adds that one facet. If the item has no room left — fifteen criteria is the ceiling on any plan — the whole apply is refused with 429 TOO_MANY_REQUESTS and nothing is written, so you never end up with half a checklist.
A work item filed in a group takes its facets at the moment it is filed. Applying a type after the fact is a repository operation.
Change a type
An update is a full replace under the version you read: send the whole definition, including every facet you want to keep, along with the version that came back from your last read. If somebody else saved the type in between, the write is refused with 409 CONFLICT rather than quietly discarding their edit.
Open Issues then Types, choose the type, and edit it in the same builder that created it. Rows can be added, reordered, relabelled and deleted. Keep a facet's key when you are only changing its wording — the key is what ties the facet to the criteria already sitting on live work. Save reloads the type if somebody changed it while you were editing, and tells you so.
gitmoap issue-blueprints update-repo-blueprint \ --repo-id <id> \ --blueprint-id <id> \ --base-type epic|story|bug|task|spike|chore|incident|sub_task \ --name <name> \ --version <n>curl -X PUT https://api.gitmoap.example/repositories/{repo_id}/issue-blueprints/{blueprint_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "base_type": "<one of bug | chore | epic | incident | spike | story | sub_task | task>", "facets[].category": "<string>", "facets[].key": "<string>", "facets[].label": "<string>", "name": "<string>", "version": "<int64>" }'Open the group's Issues then Types and edit the row. Every repository beneath the group sees the new definition on its next read of the picker; items already filed are untouched until somebody applies the type again.
gitmoap issue-blueprints update-group-blueprint \ --group-id <id> \ --blueprint-id <id> \ --base-type epic|story|bug|task|spike|chore|incident|sub_task \ --name <name> \ --version <n>curl -X PUT https://api.gitmoap.example/groups/{group_id}/issue-blueprints/{blueprint_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "base_type": "<one of bug | chore | epic | incident | spike | story | sub_task | task>", "facets[].category": "<string>", "facets[].key": "<string>", "facets[].label": "<string>", "name": "<string>", "version": "<int64>" }'Removing a facet from a definition stops it being seeded onto anything new. It does not remove it from work that already carries it — delete the criterion on those items if that is what you meant.
Retire a type without breaking anything
Archiving is the safe retirement: the type stays exactly where it is, keeps its facets and its bindings, and is marked archived so the type picker stops offering it. Archived is a field on the definition, so you archive a type by saving it — the same full-replace save described under Change a type — with that field set to true. Because the save is a full replace, a later save that leaves the field out brings the type back out of the archive; send it every time.
Archiving is a signal to the pickers, not a lock. The listing still returns an archived type, and a request that names its id can still file or apply with it, so archiving stops people choosing the type by accident rather than stopping the type working. Delete it when the id has to stop working altogether.
Note
Archive before you delete. Archiving is reversible and keeps the definition — with every facet, hint and binding you tuned — recoverable. It does cost a slot against your per-container limit, which is almost always the better trade.
Delete a type
Warning
Deleting a type cannot be undone, and it is not the same as archiving. Every work item filed against it keeps its acceptance criteria, with their ticks and their evidence — nothing about a Definition of Done is destroyed. What is destroyed is the definition itself: its facet list, its categories, its hints and its bindings are gone, and nothing can be filed with the type or have it applied from then on. The id falls free the moment the type goes, so the only repair for an accidental delete is to define it again with the same id, the same facet keys and the same bindings — and a facet key you do not reproduce exactly will seed a second criterion beside the old one the next time somebody applies the type.
Open Issues then Types, find the type, and choose Delete from the row menu. The confirmation names the type and warns that the definition, not the criteria it seeded, is what goes.
gitmoap issue-blueprints delete-repo-blueprint \ --repo-id <id> \ --blueprint-id <id>curl -X DELETE https://api.gitmoap.example/repositories/{repo_id}/issue-blueprints/{blueprint_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Open the group's Issues then Types and choose Delete from the row menu. Deleting a group type removes it from every repository's picker at once; a repository that defined its own type with the same id is unaffected and simply stops being a shadow.
gitmoap issue-blueprints delete-group-blueprint \ --group-id <id> \ --blueprint-id <id>curl -X DELETE https://api.gitmoap.example/groups/{group_id}/issue-blueprints/{blueprint_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Reference
11 endpoints
GET/repositories/{repo_id}/issue-blueprintsissue_blueprint:read
List a repository's available custom types
Token scope
issue_blueprint:read
Send
Path
- repo_iduuidRepository ID
Query
- includestringoptionalSet to
inheritedto include ancestor-group blueprints
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository not found
Receive
No body.
POST/repositories/{repo_id}/issue-blueprintsissue_blueprint:manage
Define a custom work-item type on a repository
Token scope
issue_blueprint:manage
Counts against
blueprints_per_scope
Send
Path
- repo_iduuidRepository ID
Body
- base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- blueprint_idstring
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestring
Refuses
400Invalid facets, or a bound facet on a group-only base type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent409A blueprint with this id already exists in the scope429Per-scope blueprint limit reached
Receive
- archivedbooleanoptionalHidden from the type picker; items already filed keep their
type_profile. - base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- blueprint_idstring
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestringThe custom type's display name, such as "API Feature".
- scope_idstring
- scope_typegrouprepository
- updated_atstring
- versionint64optionalThe blueprint's revision, incremented on every change. An update must send the version it
read, and is refused with a
409if the blueprint changed since.
GET/repositories/{repo_id}/issue-blueprints/{blueprint_id}issue_blueprint:read
Get one of a repository's custom types
get_repo_blueprintGET /repositories/{repo_id}/issue-blueprints/{blueprint_id}Open in the API referenceToken scope
issue_blueprint:read
Send
Path
- repo_iduuidRepository ID
- blueprint_idstringBlueprint id (slug)
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Blueprint not found
Receive
The same shape create_repo_blueprint returns — 12 fields. Go to it
Show it here
- archivedbooleanoptionalHidden from the type picker; items already filed keep their
type_profile. - base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- blueprint_idstring
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestringThe custom type's display name, such as "API Feature".
- scope_idstring
- scope_typegrouprepository
- updated_atstring
- versionint64optionalThe blueprint's revision, incremented on every change. An update must send the version it
read, and is refused with a
409if the blueprint changed since.
PUT/repositories/{repo_id}/issue-blueprints/{blueprint_id}issue_blueprint:manage
Replace a repository's custom type
update_repo_blueprintPUT /repositories/{repo_id}/issue-blueprints/{blueprint_id}Open in the API referenceToken scope
issue_blueprint:manage
Send
Path
- repo_iduuidRepository ID
- blueprint_idstringBlueprint id (slug)
Body
- archivedbooleanoptional
- base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestring
- versionint64The
versionyou last read. The update is refused with a409if the blueprint changed since.
Refuses
400Invalid facets401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Blueprint not found409The blueprint was modified concurrently
Receive
The same shape create_repo_blueprint returns — 12 fields. Go to it
Show it here
- archivedbooleanoptionalHidden from the type picker; items already filed keep their
type_profile. - base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- blueprint_idstring
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestringThe custom type's display name, such as "API Feature".
- scope_idstring
- scope_typegrouprepository
- updated_atstring
- versionint64optionalThe blueprint's revision, incremented on every change. An update must send the version it
read, and is refused with a
409if the blueprint changed since.
DELETE/repositories/{repo_id}/issue-blueprints/{blueprint_id}issue_blueprint:manage
Delete a repository's custom type
delete_repo_blueprintDELETE /repositories/{repo_id}/issue-blueprints/{blueprint_id}Open in the API referenceToken scope
issue_blueprint:manage
Send
Path
- repo_iduuidRepository ID
- blueprint_idstringBlueprint id (slug)
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Blueprint not found409The request conflicts with the resource's current state; `reason` says how
Receive
No body.
GET/groups/{group_id}/issue-blueprintsissue_blueprint:read
List a group's available custom types
Token scope
issue_blueprint:read
Send
Path
- group_iduuidGroup ID
Query
- includestringoptionalSet to
inheritedto include ancestor-group blueprints
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Group not found
Receive
No body.
POST/groups/{group_id}/issue-blueprintsissue_blueprint:manage
Define a custom work-item type on a group
Token scope
issue_blueprint:manage
Counts against
blueprints_per_scope
Send
Path
- group_iduuidGroup ID
Body
- base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- blueprint_idstring
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestring
Refuses
400Invalid facets, or a bound facet on a group-only base type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent409A blueprint with this id already exists in the scope429Per-scope blueprint limit reached
Receive
The same shape create_repo_blueprint returns — 12 fields. Go to it
Show it here
- archivedbooleanoptionalHidden from the type picker; items already filed keep their
type_profile. - base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- blueprint_idstring
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestringThe custom type's display name, such as "API Feature".
- scope_idstring
- scope_typegrouprepository
- updated_atstring
- versionint64optionalThe blueprint's revision, incremented on every change. An update must send the version it
read, and is refused with a
409if the blueprint changed since.
GET/groups/{group_id}/issue-blueprints/{blueprint_id}issue_blueprint:read
Get one of a group's custom types
Token scope
issue_blueprint:read
Send
Path
- group_iduuidGroup ID
- blueprint_idstringBlueprint id (slug)
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Blueprint not found
Receive
The same shape create_repo_blueprint returns — 12 fields. Go to it
Show it here
- archivedbooleanoptionalHidden from the type picker; items already filed keep their
type_profile. - base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- blueprint_idstring
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestringThe custom type's display name, such as "API Feature".
- scope_idstring
- scope_typegrouprepository
- updated_atstring
- versionint64optionalThe blueprint's revision, incremented on every change. An update must send the version it
read, and is refused with a
409if the blueprint changed since.
PUT/groups/{group_id}/issue-blueprints/{blueprint_id}issue_blueprint:manage
Replace a group's custom type
update_group_blueprintPUT /groups/{group_id}/issue-blueprints/{blueprint_id}Open in the API referenceToken scope
issue_blueprint:manage
Send
Path
- group_iduuidGroup ID
- blueprint_idstringBlueprint id (slug)
Body
- archivedbooleanoptional
- base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestring
- versionint64The
versionyou last read. The update is refused with a409if the blueprint changed since.
Refuses
400Invalid facets401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Blueprint not found409The blueprint was modified concurrently
Receive
The same shape create_repo_blueprint returns — 12 fields. Go to it
Show it here
- archivedbooleanoptionalHidden from the type picker; items already filed keep their
type_profile. - base_typebugchoreepicincidentspikestorysub_tasktaskThe kind of work item. The type decides where an item may live (a group or a repository), which types it may have as children, and which fields it accepts.
- blueprint_idstring
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- descriptionobjectoptional
facets[]object6 fields
- bindingobjectoptionalHow the criterion a facet adds is bound:
manual(the default), orboundto evidence the platform checks. A blueprint whose base type lives only in a group, such asepic, may use onlymanual. - categorystring
- hintobjectoptionalThe starter criterion's text. When absent the criterion is titled by the facet
label. - keystring
- labelstringThe facet's display name, such as "Observability".
- requiredbooleanoptionalWhether the facet's criterion must be satisfied before the work item can be closed or
verified. Defaults to
true.
- bindingobjectoptionalHow the criterion a facet adds is bound:
- namestringThe custom type's display name, such as "API Feature".
- scope_idstring
- scope_typegrouprepository
- updated_atstring
- versionint64optionalThe blueprint's revision, incremented on every change. An update must send the version it
read, and is refused with a
409if the blueprint changed since.
DELETE/groups/{group_id}/issue-blueprints/{blueprint_id}issue_blueprint:manage
Delete a group's custom type
delete_group_blueprintDELETE /groups/{group_id}/issue-blueprints/{blueprint_id}Open in the API referenceToken scope
issue_blueprint:manage
Send
Path
- group_iduuidGroup ID
- blueprint_idstringBlueprint id (slug)
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Blueprint not found409The request conflicts with the resource's current state; `reason` says how
Receive
No body.
POST/repositories/{repo_id}/issues/{issue_id}/apply-blueprintissue:update
Apply a custom type's facets to an existing work item
apply_blueprintPOST /repositories/{repo_id}/issues/{issue_id}/apply-blueprintOpen in the API referenceToken scope
issue:update
Counts against
criteria_per_issue
Send
Path
- repo_iduuidRepository ID
- issue_idint64Work-item iid
Body
blueprintobject2 fields
- blueprint_idstring
- scopeobjectA group or a repository, whichever a setting belongs to.
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Work item not found, or no such blueprint on this repository or the groups above it409The request conflicts with the resource's current state; `reason` says how429Per-issue or per-repository criteria limit reached
Receive
No body.
Limits
Two of the limits here scale with your plan. Both are checked as part of the write, so a refused request creates nothing at all — no half-saved type and no partial checklist.
| What is counted | Free | Pro | Team | Enterprise |
|---|---|---|---|---|
| Custom types defined on one repository or group | 25 | 50 | 200 | 500 |
| Acceptance criteria carrying a binding, across one repository | 2,000 | 5,000 | 20,000 | 50,000 |
The type count is of the types a container defines itself. Inherited ones are free, which is the direct incentive the design intends: define api-feature once on the organisation's root group rather than twenty-eight times, and you spend one slot instead of twenty-eight. Reaching the ceiling answers 429 TOO_MANY_REQUESTS; delete a type nobody files with, move the definition up to a group, or have an Owner request a limit increase from the group's usage page. Archiving does not help here — an archived type leaves the picker but keeps its slot.
The bound-criteria count is per repository and counts only criteria that carry a binding — manual checkboxes are not counted at all. It exists because every bound criterion is re-checked against every pipeline run in that repository.
The rest are the same on every plan, because they are product shape rather than capacity:
| What is counted | Limit |
|---|---|
| Facets in one type | 25 |
| Acceptance criteria on one work item | 15 |
| Characters in a facet label or hint | 200 |
| Characters in a type id, facet key or category | 40 |
Going over the facet, label or id limits is a 400 BAD_REQUEST when you save the type. The per-item criteria limit is the one that binds when the type reaches an item rather than when you save it: a type with twenty facets is perfectly legal to define and is refused with 429 TOO_MANY_REQUESTS the moment you file or apply it, because fifteen is all an item holds. If a type keeps hitting that, it is describing more than one piece of work — split it, and let the facets that are really their own job become their own items.
Troubleshooting
400 BAD_REQUESTon any call, mentioning an id
- Cause
- A path value is not well formed — usually a path like
my-group/my-repopasted where an id belongs, or a work-item number that is not a number - What to do
- Use the container's id and the item's numeric reference.
#412is written412in a URL
400 BAD_REQUESTnaming the type id, a facet key or a category
- Cause
- A slug broke its rules: it is empty, longer than 40 characters, starts with a hyphen, or contains something other than lowercase letters, digits and hyphens
- What to do
- Rewrite it as a slug —
api-feature, notAPI Feature. The display name is the field that takes capitals and spaces
400 BAD_REQUESTsaying a type must have a name, or a facet must have a label
- Cause
- The field is empty or is empty once the spaces are trimmed
- What to do
- Give it a name. The name is what people pick from the type selector, so it is not optional
400 BAD_REQUESTabout a duplicate facet key
- Cause
- Two facets in the same type share a key, and the key is the identity a re-apply matches on
- What to do
- Rename one of the keys. Two facets may share a category and a label; they may never share a key
400 BAD_REQUESTabout the number of facets
- Cause
- The type declares more than 25 facets
- What to do
- Cut it down, or split the type in two. A type with more facets than an item can hold criteria is refused the moment it is applied anyway
400 BAD_REQUESTabout a label or hint being too long
- Cause
- A facet label or hint is over 200 characters
- What to do
- Shorten it. Detail that does not fit belongs in the item's description, not in a checklist line
400 BAD_REQUESTsaying a bound facet cannot sit on a group-scoped type
- Cause
- The type is based on
epic, which lives only in a group, and a group has no pipelines whose evidence could decide the facet - What to do
- Make the facet manual, or base the type on
story,bugortaskso it lives in a repository
400 BAD_REQUESTnaming a binding argument
- Cause
- A binding is incomplete or out of range — a test binding with no test, a metric with no measurement, a coverage binding with no path or a percentage outside 0 to 100
- What to do
- Fill in the binding, or set the facet back to manual and bind it later
400 BAD_REQUESTabout an unrecognised field
- Cause
- A top-level field of the request body is one the API does not define — often a typo, or a field from an older script
- What to do
- Send only the documented top-level fields. A misspelt one is refused rather than ignored, so the call fails immediately instead of silently doing half of what you meant
401 UNAUTHORIZED
- Cause
- No credential was sent, or the one sent is invalid or expired
- What to do
- Sign in again, or mint a fresh token. Only a public container answers a read with no credential — an internal one refuses until you sign in, and no container answers a write
403 FORBIDDENon defining, changing or deleting a type, where reading works
- Cause
- Managing types needs Maintainer, because a type reaches every item filed against it
- What to do
- Ask an Owner or Maintainer to define it once. After that, anybody with Developer access can apply it
403 FORBIDDENon applying a type
- Cause
- Applying writes acceptance criteria, which needs Developer on the repository
- What to do
- Ask for Developer access, or ask somebody who has it to apply the type for you
403 INSUFFICIENT_SCOPE
- Cause
- The token is valid but was minted without the scope this call needs
- What to do
- Mint a new token carrying the scope named in the response —
issue_blueprint:read,issue_blueprint:manageorissue:update. Scopes are fixed when the token is created
404 NOT_FOUNDon a repository or group you are sure exists
- Cause
- Either it is private and you are not a member, or the id is wrong
- What to do
- A private container answers
404rather than403so its existence stays private. Check your membership, then check the id
404 NOT_FOUNDon a type that is right there in the picker
- Cause
- You asked the repository for a type it inherits. Reading, editing and deleting a type all address the container that defines it
- What to do
- Read it from the group that owns it — the listing tags every entry with where it comes from
404 NOT_FOUNDon apply, naming the type
- Cause
- The type reference names the wrong container — it has to name the container the type is defined on, not the repository the item lives in — or it names a type this repository does not offer. Only a type defined on the repository itself or on a group above it can be applied, and the same holds for filing a new item as a type
- What to do
- Take the container from the repository's own listing of available types, then apply again. A type defined on a sibling repository or another team's group is not offered here: define it on a group both repositories sit under
404 NOT_FOUNDon apply, naming the work item
- Cause
- The item number is wrong, or the item has been moved to another repository or deleted. A move gives it a new number
- What to do
- Search for the title, then apply against the repository it is in now
409 CONFLICTon defining a type
- Cause
- A type with that id already exists in this container
- What to do
- Pick another id, or edit the existing one. Nothing about the existing type is changed by the refused call
409 CONFLICTon saving an existing type, saying somebody else modified it
- Cause
- Another person saved the type after you read it, and a full replace under a stale version would discard their edit
- What to do
- Re-read the type, re-apply your change on top of what is there now, and save again with the version that read returned
429 TOO_MANY_REQUESTSon defining a type
- Cause
- The container is at its limit for types it defines itself
- What to do
- Delete one nobody files with — archiving hides a type but keeps its slot — or move the definition to a group so every repository shares one slot. An Owner can request a limit increase from the group's usage page
429 TOO_MANY_REQUESTSon apply, mentioning the work item
- Cause
- The item cannot hold the criteria the type would add — fifteen per item, on every plan
- What to do
- Remove criteria the item does not need, or split the work: facets that are really separate jobs are better as child items
429 TOO_MANY_REQUESTSon apply, mentioning the repository
- Cause
- The type carries bound facets, and the repository is at its ceiling for criteria that carry a binding
- What to do
- Delete bound criteria on closed work in that repository, or set the facet back to manual in the type and bind it on the individual items that need proving. Manual criteria are not counted at all
A type is missing from a repository's picker
- Cause
- It is archived, it is defined on a sibling group rather than an ancestor, or the listing was read without asking for inherited types
- What to do
- Ask for the inherited types, check that the group defining it really is above this repository, and check the type is not archived
Two containers define the same id and only one appears
- Cause
- Nearer definitions shadow further ones, so a repository's own
api-featurehides the group's - What to do
- Rename one of them if you meant to offer both. If you meant to override locally, that is exactly what is happening
A type was edited and nothing changed on existing items
- Cause
- Editing a definition never reaches back into work already filed — that is what stops a wording change from rewriting live checklists
- What to do
- Apply the type again to the items that should follow it. Only the missing facets are added
Applying a type added nothing
- Cause
- Every facet key it declares is already on the item. Renaming a facet's label keeps its key, so a relabelled facet counts as present
- What to do
- Read the item's Definition of Done — the criterion is there under its old wording. Change the facet's key if you genuinely want a second one
A facet's checkbox cannot be ticked
- Cause
- The facet is bound, so its state is a function of the evidence a pipeline reports, not of anybody's opinion
- What to do
- Run the pipeline that produces the evidence, or change the criterion's binding on the item to make it manual
A type was applied and the item still reads as its base type
- Cause
- The facets are what a blueprint gives an item. Applying seeds them as acceptance criteria and changes nothing else — not the item's type, not its fields, not the children it may hold
- What to do
- Read the Definition of Done, which is where the change landed. Change the item's type separately if you need the structure to move
A facet did not stop an item from closing
- Cause
- Either the facet is not required — optional facets show and roll up but do not gate — or no gate is switched on. Both gates are off by default
- What to do
- Save the type with that facet required and apply it to the items that should be gated; existing criteria keep the setting they were seeded with. Then require verified criteria before a close in the container's work-item settings, and require green criteria before a merge in its merge request settings
A saved type came back out of the archive
- Cause
- A save is a full replace, and the request left the archived field out, so it was cleared
- What to do
- Read the type, set the archived field on the body you send back, and save again
A bound facet stays unchecked after the work is done
- Cause
- No pipeline run has reported evidence that matches the binding — most often the binding names a test id, a measurement or a node that the run reports under a different name
- What to do
- Run the pipeline on the branch, then compare the name in the binding against the name in the run's report and fix whichever is wrong
Examples
A type for an endpoint-shaped story, with three required facets and one optional one, and the test facet bound so a pipeline decides it. This is the body of a create call against a repository or a group:
{ "blueprint_id": "api-feature", "name": "API Feature", "base_type": "story", "description": "A new endpoint, and everything that ships alongside it.", "facets": [ { "key": "endpoint", "label": "Endpoint", "category": "interface", "hint": "The handler, its request and response shapes, and the access rule it maps to." }, { "key": "tests", "label": "Tests", "category": "quality", "binding": { "mode": "bound", "binding": { "kind": "test", "suite": "api", "test_id": "refund_endpoint_rejects_double_refund" } } }, { "key": "observability", "label": "Observability", "category": "operability", "hint": "The endpoint emits a request metric and an error metric." }, { "key": "docs", "label": "Documentation", "category": "docs", "required": false } ]}Applying it to work already filed. Replace <group-id> with the id of the group the type is defined on — the listing tags every entry with the container it came from, and that is the container to name here even when you are applying to a repository's item:
{ "blueprint": { "scope": { "group": "<group-id>" }, "blueprint_id": "api-feature" }}A type defined on a repository is named the same way, with repo in place of group and the repository's id. The item itself is addressed by the path, so the repository in the path and the container in the body are different things and are meant to be.
Later, the team decides documentation is not optional and adds a fifth facet. The update is a full replace, so send every facet you are keeping, with the same keys, plus the version you last read — then apply the type again to the items that should follow it.
FAQ
Do inherited types count against my limit?
No. The limit counts the types a container defines itself, so a type defined once on a root group and offered to fifty repositories costs one slot, not fifty.
Can I change a type's base type?
Yes, on any save. It changes what new items filed with that type become; items already filed keep the base type they were created with, because retyping live work is a separate decision with its own rules. Moving the base type between a group type and a repository type also moves where the type can be filed, so check that the containers using it can still take it.
Can a facet become its own work item?
Not in one action. When a facet is really a chunk of work with its own owner and its own discussion, file a sub-task for it and let the parent's criteria track the rest.
Does a custom type change how items are listed, filtered or rolled up?
No. Every listing, board, filter and rollup keys on the base type, so an item filed as "API Feature" appears everywhere a story appears. The shared vocabulary and the seeded facets are what you gain; nothing about the item's structure is traded for them.
Can I stop people filing with a type without losing it?
Archive it. It leaves the picker, keeps every facet and binding you tuned, and comes back the moment you save it unarchived. It is a signal rather than a lock, though — a request that names the id still works — so delete the type when the id has to stop working. An archived type also holds its slot against the per-container limit while it sits there.
Related
- Proof of Doneacceptance criteria, what a binding can prove, and the merge gate that reads them
- Work itemsfiling an item, the eight base types, and what each one may contain
- Linking work to codeconnecting an item to the commits, merge requests and lines that satisfy it
- Merge request settingsthe gate that holds a merge until every required criterion on a linked item is green
- Issuesthe rest of the work tracker, and where blueprints sit in it
- Billing and planswhat each plan includes, and how to ask for a limit increase