Endpoints — Schema
Every call in this family: what the card records, the edge cases that live on the call, and the verdict of every entry that measured it. Each of those lines names the door holding that entry's rules. The map is corpus/INDEX.md.
GET /schema
The enabled type list, and the enablement test for a CustomEntityNN: a slot absent here 404s everywhere. 12KB, so fetch it once and never loop it into /fields.
name.valueis the display name and is what a person recognises.CustomEntity19is what the URL takes. Read the first, address by the second, and never hardcode a slot number: they are non-contiguous and site-specific.The value is a property object, not a string.
data["Version"]["name"]["value"]is two levels deeper than it looks.Presence here is the enablement test. A slot absent from this listing 404s everywhere else.
106 types here against 71 fields on one of them. This call is cheap and
/fieldsis not, so take the list from here and fetch fields only for the types you need.
Measured by
062_cors(findings) — Every path under/api/v1answers the preflight and echoes anyOrigin, credentials true./internal_apiand the web paths send no CORS header, so a page on another origin proxies those.
rules:doors/findings-protocol002_schema(findings) — Fetch /schema once for the type list, then /schema//fields only for types you actually need: it is the expensive call (48KB, ~330ms each) and must never be looped over all types.
rules:doors/findings-schema008_custom_entities(findings) — Presence in /schema is the enablement test for a custom entity: a slot absent from the listing 404s. Slot numbers are non-contiguous and site-specific, so read name.value and never hardcode one.
rules:doors/findings-schema
corpus/endpoints/get_schema.md
GET /schema/<Type>
One type's display name without its 48KB of fields, and the cheapest existence check there is: an unknown or unenabled type is 404 Entity type 'X' does not exist.
The two 404s are byte-identical apart from the name, so this call cannot tell "your site has not enabled that slot" from "you invented a type". Both mean the same to a caller: do not address it.
138 bytes against 47958 for the same type's
/fields. Checking existence here rather than there is the difference between one call and a page of them.
Measured by
028_loud_and_silent(findings) — A 400 is trustworthy and usually names the legal set, but a 200 proves nothing: an unknown field, sort key or query param is a no-op, and a batch can return an id for a row it never made.
rules:doors/findings-protocol002_schema(findings) — Fetch /schema once for the type list, then /schema//fields only for types you actually need: it is the expensive call (48KB, ~330ms each) and must never be looped over all types.
rules:doors/findings-schema
Silent on this call
028_loud_and_silent— A 400 is trustworthy and usually names the legal set, but a 200 proves nothing: an unknown field, sort key or query param is a no-op, and a batch can return an id for a row it never made.
corpus/endpoints/get_schema_type.md
GET /schema/<Type>/fields
Every field on one type with its data_type, editable and mandatory. The expensive call at 48KB and ~330ms, so fetch the types you need and never loop the /schema listing into it.
mandatoryis not the create contract.codereadsmandatory: trueand a create omitting it succeeds at 201 with a server-invented name;projectreadsmandatory: falseand a create omitting it is 400. Read the create contract from the entity-type card, not from this flag.Every value is wrapped in
{value, editable}, and the outereditablesays whether you may change the property, not whether you may write the field.data["code"]["editable"]["value"]is the one that answers "can I write this".Adding
project_idchanges the body by 28 bytes on the probed site: onlyhidden_valuesappears. Everything else is identical at every scope.48KB and about 330ms per type. Never loop this over the
/schemalisting.
Measured by
002_schema(findings) — Fetch /schema once for the type list, then /schema//fields only for types you actually need: it is the expensive call (48KB, ~330ms each) and must never be looped over all types.
rules:doors/findings-schema019_create_fields(findings) — Custom fields are creatable over REST, but you pass a display name and a duplicate silently becomes_1: an idempotent ensure() must read /schema first, never POST-and-hope.
rules:doors/findings-schema056_stock_vs_custom_field(findings) — A field withvisible.editablefalse is stock and safe to depend on; true means the site can hide it, which is every custom field and a few stock ones. Thesg_prefix decides nothing.
rules:doors/findings-schema061_shipped_statuses(findings) — Nothing in the schema marks a shipped Status.systemis true on a minority of them; the stock set iscreated_by is null, plusoptions[return_only]=retiredfor the rows a site retired.
rules:doors/findings-schema007_fill_rates(findings) — On the sample project 30 of 71 Version fields are populated. Rank by fill rate, but drop checkbox, summary and computed fields first: False and 0 are not null and read as 100% filled.
rules:doors/findings-read023_pages(findings) — A page's layout is the PageSetting row whose user is null; settings_json reads back as decoded JSON and body/list_content settings.columns is the column list. Every filter on it is ignored.
rules:doors/findings-read068_note_read_state(findings) — read_by_current_user is per person and missing from the schema;isandis_notare evaluated, whilein,not_inand an unknownisvalue all return the unread rows at 200.
rules:doors/findings-filter071_note_link_name_filter(findings) — Filter notes about a thing onnote_links.<Type>.cached_display_name: it resolves for every valid type,code400s on Booking andnameon all but Department. The path cannot be read back.
rules:doors/findings-filter011_create_project(findings) — A script user can create a Project with nothing but {"name": ...}, at 201, but the response echoes only 6 attributes, so read the project back if you need anything else.
rules:doors/findings-write012_create_version(findings) — The schema's mandatory flags are not the create contract: on every project-scoped type measured,projectis required and the identity field is optional, server-generated and not unique.
rules:doors/findings-write069_client_note(findings) —client_notecannot be set over REST:trueon create is 400 and anyPUTis 400editable on create only.sg_note_type: "Client"is the one marker a caller can write.
rules:doors/findings-write070_authored_timestamps(findings) — A create body sets created_at and updated_at and they read back exactly, on Note, Task and Version, though the schema flags both editable false; every PUT on either 400s.
rules:doors/findings-write025_event_log(findings) — meta.old_value and meta.new_value answer "what was this before", but meta is unfilterable and unsortable: narrow on entity, event_type and attribute_name, sort -id, read meta yourself.
rules:doors/findings-observe
Silent on this call
019_create_fields— Custom fields are creatable over REST, but you pass a display name and a duplicate silently becomes_1: an idempotent ensure() must read /schema first, never POST-and-hope. 023_pages— A page's layout is the PageSetting row whose user is null; settings_json reads back as decoded JSON and body/list_content settings.columns is the column list. Every filter on it is ignored.068_note_read_state— read_by_current_user is per person and missing from the schema;isandis_notare evaluated, whilein,not_inand an unknownisvalue all return the unread rows at 200.025_event_log— meta.old_value and meta.new_value answer "what was this before", but meta is unfilterable and unsortable: narrow on entity, event_type and attribute_name, sort -id, read meta yourself.
corpus/endpoints/get_schema_type_fields.md
POST /schema/<Type>/fields
You send a display name and the server derives the sg_ name, which is only in links.self. A duplicate display name is 201 with a silent _1 suffix, so read /fields first.
Display name to programmatic name, measured:
| display name sent | name created |
|---|---|
zzprobe 019 With (Parens) |
sg_zzprobe_019_with__parens_ |
sg_zzprobe_019_already_prefixed |
sg_sg_zzprobe_019_already_prefixed, prefixed twice |
| a name already in use | <name>_1, at 201, with no warning |
Deleting a field does not free its name.
DELETEretires the field and the name stays taken, released only by emptying the Trash page in the web interface. Anensure()reads/fieldsfirst rather than posting and hoping, and a field created to test with is namedsg_zzprobe_<nnn>_*so whoever empties that page can tell litter from a real field.numbertakes 2147483647 and 400s on 2**63, so a 64-bit id or seed does not fit one.
Measured by
019_create_fields(findings) — Custom fields are creatable over REST, but you pass a display name and a duplicate silently becomes_1: an idempotent ensure() must read /schema first, never POST-and-hope.
rules:doors/findings-schema040_field_revive(findings) — A trashed field is revived by POST /schema//fields/ with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.
rules:doors/findings-schema006_duplicate_field_name_is_201(reports) — Creating a custom field whose display name is taken answers 201 for a suffixed field instead of a conflict, and every retry burns a programmatic name no REST call frees.
rules:doors/reports
Silent on this call
post_schema_type_fields— You send a display name and the server derives thesg_name, which is only inlinks.self. A duplicate display name is 201 with a silent_1suffix, so read/fieldsfirst.019_create_fields— Custom fields are creatable over REST, but you pass a display name and a duplicate silently becomes_1: an idempotent ensure() must read /schema first, never POST-and-hope. 040_field_revive— A trashed field is revived by POST /schema//fields/ with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.
corpus/endpoints/post_schema_type_fields.md
GET /schema/<Type>/fields/<field>
One field's properties, at 1211 bytes against 48KB for the whole type. Pass project_id or hidden_values is empty and your status picker offers statuses the project refuses.
Without
project_id,hidden_valuesis empty and the same 16 come back. A picker built on the site-scope answer offers statuses the project's own interface refuses.REST does not enforce the subtraction on write. A hidden status writes and reads back fine, so every client subtracts
hidden_valuesitself.The 404 names the type and the field together,
Version.sg_not_a_field, which is the only error on the schema endpoints that says which half you got wrong.A 200 with
data: nullis a third answer, not an empty one.Note.read_by_current_useris on every Note, filters and takes a write, and is in neitherGET /schema/Note/fieldsnor this call'sdata. Ask this endpoint, not the field census, before concluding a field is absent (probe 068).
Measured by
028_loud_and_silent(findings) — A 400 is trustworthy and usually names the legal set, but a 200 proves nothing: an unknown field, sort key or query param is a no-op, and a batch can return an id for a row it never made.
rules:doors/findings-protocol002_schema(findings) — Fetch /schema once for the type list, then /schema//fields only for types you actually need: it is the expensive call (48KB, ~330ms each) and must never be looped over all types.
rules:doors/findings-schema009_status_lists(findings) — A project's usable statuses are valid_values minus hidden_values, read with project_id: valid_values is identical at every scope, hidden_values is the only thing that varies.
rules:doors/findings-schema056_stock_vs_custom_field(findings) — A field withvisible.editablefalse is stock and safe to depend on; true means the site can hide it, which is every custom field and a few stock ones. Thesg_prefix decides nothing.
rules:doors/findings-schema060_entity_dict_name(findings) — Thenamein an entity dict is the target'scached_display_name, filled on every type measured, single and multi alike. Read it, not the per-type identity field, and expect decoration.
rules:doors/findings-read068_note_read_state(findings) — read_by_current_user is per person and missing from the schema;isandis_notare evaluated, whilein,not_inand an unknownisvalue all return the unread rows at 200.
rules:doors/findings-filter049_script_events(findings) — A script's writes reach the event log only while its ApiUser has generate_event_log_entries True. The default is False and nothing errors when off. One create logs one row per field plus one _New.
rules:doors/findings-observe003_query_fields_and_pages(recipes) — Resolve a query field's value, and run the rows a saved Page shows
rules:doors/recipes005_propagate_status(recipes) — Roll a status up from a parent's Tasks and Versions onto the parent, without racing a concurrent write
rules:doors/recipes008_delivery_progress(recipes) — Keep a Delivery honest about what a long transfer is doing, including when it is cancelled and when it crashes
rules:doors/recipes010_status_picker(recipes) — List the statuses a project actually offers, each with the label, colour and icon needed to draw it
rules:doors/recipes
Silent on this call
028_loud_and_silent— A 400 is trustworthy and usually names the legal set, but a 200 proves nothing: an unknown field, sort key or query param is a no-op, and a batch can return an id for a row it never made.068_note_read_state— read_by_current_user is per person and missing from the schema;isandis_notare evaluated, whilein,not_inand an unknownisvalue all return the unread rows at 200.049_script_events— A script's writes reach the event log only while its ApiUser has generate_event_log_entries True. The default is False and nothing errors when off. One create logs one row per field plus one _New.005_propagate_status— Roll a status up from a parent's Tasks and Versions onto the parent, without racing a concurrent write
corpus/endpoints/get_schema_type_fields_field.md
POST /schema/<Type>/fields/<field>
Revive a retired field, at 204. It is the only way to get a burnt name back, and it returns at its original data_type whatever the site wants now.
The revived field keeps its original
data_type. A name burnt astextcannot come back asnumber, and thePUTthat would change it is a 200 that does nothing.The API named this parameter itself, in the 400 for an empty body. Nothing in the documentation does.
Measured by
040_field_revive(findings) — A trashed field is revived by POST /schema//fields/ with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.
rules:doors/findings-schema
Silent on this call
040_field_revive— A trashed field is revived by POST /schema//fields/ with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.
corpus/endpoints/post_schema_type_fields_field.md
PUT /schema/<Type>/fields/<field>
Changes a field's properties. A body changing data_type is a 200 that does nothing, so read the field back rather than trusting the status code.
data_typeis immutable and the API does not say so. The write answers 200 and the value is unchanged. This is the sharpest case of the general rule that a 200 from this API proves the request parsed, not that it happened.Renaming changes the display name only. The programmatic name is fixed at creation, and a rename does not free the old one for reuse.
Measured by
040_field_revive(findings) — A trashed field is revived by POST /schema//fields/ with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.
rules:doors/findings-schema
Silent on this call
put_schema_type_fields_field— Changes a field's properties. A body changingdata_typeis a 200 that does nothing, so read the field back rather than trusting the status code.040_field_revive— A trashed field is revived by POST /schema//fields/ with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.
corpus/endpoints/put_schema_type_fields_field.md
DELETE /schema/<Type>/fields/<field>
Retires a field at 204 and burns its programmatic name forever: the same name will not create again, only revive. Treat this as irreversible from REST.
The name is not freed. The error tells you to empty the Trash page in the web interface, which REST cannot do. From an API client the only way back is
POST /schema/<Type>/fields/<field>with{"revive": true}, and it returns at the original type.The collision is on the programmatic name alone. Recreating at a different
data_typeis the identical 400.A probe cannot clean up after itself here the way it can for a row. Test on a stock field, and where one has to be created, name it
sg_zzprobe_<nnn>_*so it is identifiable when the Trash page is emptied.
Measured by
019_create_fields(findings) — Custom fields are creatable over REST, but you pass a display name and a duplicate silently becomes_1: an idempotent ensure() must read /schema first, never POST-and-hope.
rules:doors/findings-schema040_field_revive(findings) — A trashed field is revived by POST /schema//fields/ with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.
rules:doors/findings-schema006_duplicate_field_name_is_201(reports) — Creating a custom field whose display name is taken answers 201 for a suffixed field instead of a conflict, and every retry burns a programmatic name no REST call frees.
rules:doors/reports
Silent on this call
019_create_fields— Custom fields are creatable over REST, but you pass a display name and a duplicate silently becomes_1: an idempotent ensure() must read /schema first, never POST-and-hope. 040_field_revive— A trashed field is revived by POST /schema//fields/ with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.
corpus/endpoints/delete_schema_type_fields_field.md