The Builder loop
A Builder, a person working through chat or an api_client MCP token, builds one setup object at a time: a rule, a workflow, a role, a table. It never declares a whole app as a draft and installs it in one shot (that bundle is retired). It reads what exists, asks when something structural is missing, builds one piece through the exact operation the admin page already uses for that object, tries it, then reviews the change set.
The loop has five steps: look, ask, build one piece, try it, review the change set. Every tool named below is generated straight from the operation registry; nothing here is a second, hand-kept list.
Look
Every registered kind has a list_<plural> and a get_<kind> tool (the directory reads): call them before building anything, so a new object never collides with one that already exists. The full kind-by-kind table, with every verb and its risk class:
| Kind | Risk | Verbs |
|---|---|---|
approval_policy | configuration | list_approval_policies, get_approval_policy, create_approval_policy, update_approval_policy, try_approval_policy, delete_approval_policy, restore_approval_policy (not on MCP) |
business_rule | configuration | list_business_rules, get_business_rule, create_business_rule, update_business_rule, try_business_rule, delete_business_rule, restore_business_rule |
catalog_category | configuration | list_catalog_categories, get_catalog_category, create_catalog_category, update_catalog_category, try_catalog_category |
catalog_item | configuration | list_catalog_items, get_catalog_item, create_catalog_item, update_catalog_item, try_catalog_item |
dashboard | configuration | list_dashboards, get_dashboard, create_dashboard, update_dashboard, try_dashboard, delete_dashboard |
discovery_source | configuration | list_discovery_sources, get_discovery_source, create_discovery_source, update_discovery_source, try_discovery_source, delete_discovery_source, restore_discovery_source (not on MCP) |
field | configuration | list_fields, get_field, create_field, update_field, try_field, delete_field, restore_field |
group | access | list_groups, get_group, create_group, update_group, try_group, delete_group, restore_group (not on MCP) |
menu_item | configuration | list_menu_items, get_menu_item, create_menu_item, update_menu_item, try_menu_item, delete_menu_item |
notification_rule | configuration | list_notification_rules, get_notification_rule, create_notification_rule, update_notification_rule, try_notification_rule, delete_notification_rule, restore_notification_rule |
role | access | list_roles, get_role, create_role, update_role, try_role, delete_role, restore_role (not on MCP) |
table | configuration | list_tables, get_table, create_table, update_table, try_table, delete_table, restore_table |
user_access | access | get_user_access, update_user_access, try_user_access |
workflow | configuration | list_workflows, get_workflow, create_workflow, update_workflow, try_workflow, delete_workflow, restore_workflow (not on MCP) |
Each kind’s fields
What create_<kind>/update_<kind> accept, read straight off each kind’s own spec model. A field with no description here carries none on the spec either: never guessed.
approval_policy
| Field | Type | What it means |
|---|---|---|
id | string | (no description on the spec field) |
name | string | (no description on the spec field) |
description | string | (no description on the spec field) |
is_active | boolean | (no description on the spec field) |
table | string | (no description on the spec field) |
moment | ApprovalMoment | (no description on the spec field) |
if | ApprovalIf | (no description on the spec field) |
steps | array of ApprovalStep | (no description on the spec field) |
then | ApprovalThen | (no description on the spec field) |
advanced | ApprovalAdvanced | (no description on the spec field) |
business_rule
| Field | Type | What it means |
|---|---|---|
id | string | (no description on the spec field) |
name | string | (no description on the spec field) |
description | string | (no description on the spec field) |
is_active | boolean | (no description on the spec field) |
table_name | string | (no description on the spec field) |
events | enum (created, updated, created_or_updated) | (no description on the spec field) |
match | enum (all, any) | (no description on the spec field) |
conditions | array of RuleCondition | (no description on the spec field) |
logic | enum (steps, script, formula) | (no description on the spec field) |
steps | array of any | (no description on the spec field) |
rule_type | enum (calculated, validation, action) | (no description on the spec field) |
script_body | string | (no description on the spec field) |
applies_to_field | string | (no description on the spec field) |
drafted_with | DraftedWith | (no description on the spec field) |
catalog_category
| Field | Type | What it means |
|---|---|---|
name | string | (no description on the spec field) |
description | string | (no description on the spec field) |
icon | string | (no description on the spec field) |
image_url | string | (no description on the spec field) |
parent_id | string | (no description on the spec field) |
order | integer | (no description on the spec field) |
is_active | boolean | (no description on the spec field) |
catalog_item
| Field | Type | What it means |
|---|---|---|
basics | Basics | (no description on the spec field) |
questions | array of QuestionSpec | (no description on the spec field) |
also_set | array of AlsoSet | (no description on the spec field) |
visibility | Visibility | (no description on the spec field) |
approval | any | (no description on the spec field) |
handling | Handling | (no description on the spec field) |
dashboard
| Field | Type | What it means |
|---|---|---|
name | string | (no description on the spec field) |
description | string | (no description on the spec field) |
is_public | boolean | (no description on the spec field) |
visibility | string | (no description on the spec field) |
config | object | (no description on the spec field) |
page_type | string | (no description on the spec field) |
layout_type | string | (no description on the spec field) |
layout_config | object | (no description on the spec field) |
icon | string | (no description on the spec field) |
tags | array | (no description on the spec field) |
is_default | boolean | (no description on the spec field) |
slug | string | (no description on the spec field) |
theme_config | object | (no description on the spec field) |
workspace_id | string | (no description on the spec field) |
discovery_source
| Field | Type | What it means |
|---|---|---|
name | string | (no description on the spec field) |
description | string | (no description on the spec field) |
source_type | string | ldap, network, azure_ad, aws, gcp, azure, etc. (create only) |
agent_id | string | (no description on the spec field) |
config | object | (no description on the spec field) |
credential_id | string | (no description on the spec field) |
schedule | string | (no description on the spec field) |
transform_map | object | (no description on the spec field) |
entity_types | array of string | (no description on the spec field) |
classification_mode | enum (manual, auto, disabled) | (no description on the spec field) |
trust_priority | integer | (no description on the spec field) |
status | string | (no description on the spec field) |
icon | string | (no description on the spec field) |
color | string | (no description on the spec field) |
field
| Field | Type | What it means |
|---|---|---|
name | string | (no description on the spec field) |
label | string | (no description on the spec field) |
labels | object | (no description on the spec field) |
field_type | string | (no description on the spec field) |
required | boolean | (no description on the spec field) |
in_list | boolean | (no description on the spec field) |
choices | array of ChoiceSpec | (no description on the spec field) |
reference | ReferenceSpec | (no description on the spec field) |
help | string | (no description on the spec field) |
default | any | (no description on the spec field) |
inherited_from | string | (no description on the spec field) |
hidden | boolean | (no description on the spec field) |
origin | enum (person, blueprint, platform) | (no description on the spec field) |
asked_when_creating | boolean | (no description on the spec field) |
unique | boolean | (no description on the spec field) |
indexed | boolean | (no description on the spec field) |
field_length | integer | (no description on the spec field) |
held | integer | (no description on the spec field) |
kept_by_platform | boolean | (no description on the spec field) |
min_value | number | (no description on the spec field) |
max_value | number | (no description on the spec field) |
min_length | integer | (no description on the spec field) |
readonly | boolean or string | (no description on the spec field) |
write_mode | string | (no description on the spec field) |
table | string | (no description on the spec field) |
sensitivity | enum (withheld, value, identity, open) | (no description on the spec field) |
group
| Field | Type | What it means |
|---|---|---|
name | string | (no description on the spec field) |
display_name | string | (no description on the spec field) |
description | string | (no description on the spec field) |
group_type | string | (no description on the spec field) |
department_id | string | (no description on the spec field) |
members | array of string | (no description on the spec field) |
leader_id | string | (no description on the spec field) |
status | string | (no description on the spec field) |
menu_item
| Field | Type | What it means |
|---|---|---|
title | string | (no description on the spec field) |
slug | string | (no description on the spec field) |
description | string | (no description on the spec field) |
icon | string | (no description on the spec field) |
type | MenuItemType | (no description on the spec field) |
path | string | (no description on the spec field) |
config | object | (no description on the spec field) |
parent_id | string | (no description on the spec field) |
order | integer | (no description on the spec field) |
is_visible | boolean | (no description on the spec field) |
is_disabled | boolean | (no description on the spec field) |
badge | object | (no description on the spec field) |
highlight | boolean | (no description on the spec field) |
required_permission | string | (no description on the spec field) |
permissions | object | (no description on the spec field) |
module_id | string | (no description on the spec field) |
notification_rule
| Field | Type | What it means |
|---|---|---|
id | string | (no description on the spec field) |
name | string | (no description on the spec field) |
on | boolean | (no description on the spec field) |
table | string | (no description on the spec field) |
moment | MomentSpec | (no description on the spec field) |
if | array of IfCondition | (no description on the spec field) |
who | WhoSpec | (no description on the spec field) |
how | array of enum (in_app, email) | (no description on the spec field) |
what | WhatSpec | (no description on the spec field) |
advanced | AdvancedSpec | (no description on the spec field) |
origin | enum (person, blueprint, platform) | (no description on the spec field) |
read_only_reason | object | (no description on the spec field) |
role
| Field | Type | What it means |
|---|---|---|
name | string | (no description on the spec field) |
purpose | string | (no description on the spec field) |
desk_slug | string | (no description on the spec field) |
record_grants | array of RecordGrant | (no description on the spec field) |
platform_capabilities | array of string | (no description on the spec field) |
table
| Field | Type | What it means |
|---|---|---|
name | string | (no description on the spec field) |
label | string | (no description on the spec field) |
label_singular | string | (no description on the spec field) |
labels | object | (no description on the spec field) |
description | string | (no description on the spec field) |
icon | string | (no description on the spec field) |
records | enum (work, plain) | (no description on the spec field) |
parent | string | (no description on the spec field) |
prefix | string | (no description on the spec field) |
fields | array of FieldSpec | (no description on the spec field) |
inherited | array of FieldSpec | (no description on the spec field) |
access | array of AccessGrant | (no description on the spec field) |
menu | object | (no description on the spec field) |
origin | enum (person, blueprint, platform) | (no description on the spec field) |
read_only_reason | string | (no description on the spec field) |
record_count | integer | (no description on the spec field) |
user_access
| Field | Type | What it means |
|---|---|---|
user_id | string | (no description on the spec field) |
level | string | (no description on the spec field) |
configurator_areas | array of string | (no description on the spec field) |
desk_roles | array of DeskRoleGrant | (no description on the spec field) |
team_ids | array of string | (no description on the spec field) |
roles | array of RoleGrant | (no description on the spec field) |
workflow
| Field | Type | What it means |
|---|---|---|
id | string | (no description on the spec field) |
name | string | (no description on the spec field) |
description | string | (no description on the spec field) |
is_active | boolean | (no description on the spec field) |
table_name | string | (no description on the spec field) |
when | WorkflowWhen | (no description on the spec field) |
match | enum (all, any) | (no description on the spec field) |
conditions | array of WorkflowCondition | (no description on the spec field) |
steps | array of any | (no description on the spec field) |
Ask
When a write names something that is not there, a group, a table, an approver, the tool answers a needs_decision refusal instead of a dead end:
{"success": false, "code": "needs_decision", "question": "...", "options": [{"id": "...", "label": "..."}], "refusals": [...]}Answer it by calling the same tool again with the chosen option’s id in the field the refusal named. The question text and the options come from the SAME reader the validator itself checked against, so the choices offered are always real. Every refusal code this covers today:
| Kind | Refusal code | Question |
|---|---|---|
approval_policy | policy_not_found | That approval policy was not found. Which policy did you mean? |
approval_policy | role_unknown | That role was not found. Which role should this step ask? |
catalog_category | category_parent_not_found | That parent category was not found. Which category did you mean? |
field | parent_not_installed | The parent table that field reference needs is not installed. Which table did you mean? |
field | reference_table_unreadable | That reference table cannot be read. Which readable table did you mean? |
group | group_name_held_by_archived_group | That name is held by an archived group. Reactivate it, or choose another name? |
group | group_not_in_tenant | That group is not in this workspace. Which group did you mean, or create a new one? |
menu_item | menu_item_module_not_found | That module was not found. Which installed module did you mean? |
notification_rule | who_person_not_found | That person was not found. Which person did you mean? |
notification_rule | who_role_not_found | That role was not found. Which role did you mean? |
notification_rule | who_team_not_found | That team was not found. Which group did you mean? |
role | role_not_in_tenant | That role is not in this workspace. Which role did you mean? |
role | table_not_in_registry | That table is not in this workspace’s registry. Which table did you mean? |
table | parent_not_installed | The parent table that field reference needs is not installed. Which table did you mean? |
table | reference_table_unreadable | That reference table cannot be read. Which readable table did you mean? |
user_access | role_not_in_tenant | That role is not in this workspace. Which role should this person hold? |
user_access | user_not_in_tenant | That person is not in this workspace. Which person did you mean? |
workflow | workflow_group_not_found | That group was not found. Which group did you mean? |
workflow | workflow_table_not_found | That table was not found. Which table did you mean? |
workflow | workflow_user_not_found | That person was not found. Which person did you mean? |
ask_user does not reach MCP: nothing on that surface waits for an answer. A needs_decision refusal is how the registry asks instead, and the client’s own conversation is where a person answers it.
Build one piece
create_<kind> and update_<kind> write through the kind’s own validator and writer, the exact path the admin page’s /spec endpoint already uses. A normal write answers success:
{"success": true, "kind": "...", "verb": "...", "id": "...", "result": {...}, "session_id": "..."}A work table’s lifecycle is its inherited state: list state under the table spec’s inherited with your own choices (draft, submitted, on_loan, returned, …). An approval’s then.approved_state and rejected_state move state, and rules and notices watch it. A second status field of your own is a second lifecycle nothing else moves; to set a field of your own on approval, use then.approved_set_fields. A new record starts in the first of your own states unless you give default on that inherited field. A task for people lands in a work table of your own, never in fl_tasks itself: a script’s db.insert('fl_tasks', ...) is refused.
A script rule’s body never reads back: get_business_rule answers script_body: null and script_present: true for a stored script. Judge a script rule by try_business_rule or a real record, never by its read-back. A work table gets a number prefix when you give none.
To put a person into a role you made, call update_user_access with the person’s user_id and roles: [{"role_id": ...}], keeping their level as it is (get_user_access shows it). roles left out leaves their roles alone; roles: [] takes every direct role away. A role that belongs to an installed desk goes under desk_roles instead. That change is held for a person’s approval. A Builder never makes anyone an admin: level: admin is refused, and an admin sets that level on the People page.
A new notification_rule and a new approval_policy land switched off (on: false, is_active: false), exactly as their own pages create them: try the piece, then switch it on with update_<kind>. The create answer shows the flag, so read it.
A bad payload answers a plain refusal, the same shape for every kind:
{"success": false, "code": "...", "refusals": [{"code": "...", "path": "...", "message": "..."}], "http_status": ..., "detail": ..., "session_id": "..."}Some writes do not run at all: they are HELD for a person to approve first. An api_client token’s call is always held when it grades into one of these risk classes, whatever the tenant’s own switch says: access, destructive, reach. That covers admin or platform-capability grants, role and access changes, deletes, and anything reaching outside the platform (owner decision M-D21). A held call answers a proposal, nothing written:
{"success": true, "plan_proposed": true, "plan_id": "...", "risk_grade": "...", ...}A person decides the held plan on the Approvals page. Nobody may approve their own proposal. Once approved, the SAME tool call replays through the same path every other door uses.
Try it
try_<kind> is generated for every registered kind. For approval_policy, business_rule, notification_rule, workflow, it is a real dry run against one existing record (record_id is required): nothing is written, and the refusal list it answers is exactly what a real write against that record would refuse. For every other kind, try_<kind> is the owner’s validate: the same refusal list a create/update would answer, also with nothing written. Call it between building a piece and reviewing the change set.
Review the change set
Every create, update, delete, and restore through the registry captures one change into the acting user’s active Studio session, opened automatically on the first write (start_studio_session/get_active_session to see it, get_session_changes to list what changed, pause_studio_session/resume_studio_session to step away and come back). Rolling back replays each captured change through the SAME registry operation that made it, in reverse: an update replays as another update with the before-state, a create is reversed by delete (or restore where the kind has one). An item the registry cannot undo is named as a failure, never silently skipped.