The form.* family (10 verbs). Every verb enters through the one front door: it is capability-checked, permission-checked, scope-checked and audited before it runs (see Security & permissions). Each entry below lists its capability, risk class (which fixes the minimum autonomy level), the minimum caller permission, its availability tier, whether it is idempotent, and any outbound egress.
Createform.create
| Property | Value |
|---|---|
| Capability | form.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
(DEFERRED mixed bearered/bearerless). { title, description?, schema?(={}), status?(=PUBLISHED), createdById? }. created_by_id defaults to the acting user (NOT NULL). Mints form_.
Call POST /v/form.create with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.create.
Deleteform.delete
| Property | Value |
|---|---|
| Capability | form.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
(DEFERRED, destructive). Fenced id+workspace; FK cascade clears
form_versions/form_submissions/form_favourites/form_watches. rows_affected==0 -> BadInput. { id }.
Call POST /v/form.delete with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.delete.
Createform.favourite.create
| Property | Value |
|---|---|
| Capability | form.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | Yes |
| Egress | None - in-box (pure Postgres, no outbound call) |
Idempotent INSERT … ON CONFLICT (form_id, user_id) DO NOTHING.
The form is fenced within the workspace. { formId }; userId = acting user.
Call POST /v/form.favourite.create with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.favourite.create.
Deleteform.favourite.delete
| Property | Value |
|---|---|
| Capability | form.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | Yes |
| Egress | None - in-box (pure Postgres, no outbound call) |
DELETE by (form_id, acting user, workspace); idempotent no-op if
absent (no id round-trip needed). { formId }.
Call POST /v/form.favourite.delete with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.favourite.delete.
Generateform.generate
| Property | Value |
|---|---|
| Capability | form.gen |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
The sibling of doc.generate.
NEVER auto-published (the load-bearing invariant). The save path is board.create(kind
, status=DRAFT) → board.validate → board.publish (a SEPARATE, ADMIN, deliberate step). This verb runs the lint + refuse-harmful gate (INV-9) and lands a DRAFT board only. An agent-authored schema that fails the lint is REFUSED (no silent default); a published-form is impossible from this verb.Input { prompt, title?, schema? } (fields ending in ? are optional)
Call POST /v/form.generate with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.generate.
Ingestform.ingest
| Property | Value |
|---|---|
| Capability | form.write |
| Risk class | Write - autonomy L1 |
| Min. permission | External (portal / signer token) |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | Yes |
| Egress | None - in-box (pure Postgres, no outbound call) |
The pipeline (TASK-FORM-04, fixed ordering):
- PUBLISHED-gate: a submission against a non-ACTIVE(=published) intake-form board is hard-rejected.
- Idempotency: a stable submissionId dedups a callback/retry (the second ingest is a no-op).
- For each
datafield: sanitise strings → DETECT an inlinedata:base64 file-URL → extract it into an ENCRYPTED Fact (INV-7, never a plaintext passport scan in a JSON column) and replace it with a{_type:'stored_file', storageKey, ...}ref; a plain value lands as a key-bound Fact (C7, bound to the fieldkey). - If a
matterId(+ optionalstepId) is supplied, route to matter.advance - the ONE step-completion write path (INV-1). The submission→work on-ramp is the canonical verb, not a shadow write. (A submission with no matter simply persists - intake without a spawn is valid.)
Input { formId, data, submissionId?, matterId?, stepId? } (fields ending in ? are optional)
Call POST /v/form.ingest with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.ingest.
Updateform.update
| Property | Value |
|---|---|
| Capability | form.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
(DEFERRED). Partial PATCH fenced by id+workspace. { id, title?, description?|null, schema?, status?, currentVersion? }. rows_affected==0 -> BadInput not-found.
Call POST /v/form.update with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.update.
Createform.version.create
| Property | Value |
|---|---|
| Capability | form.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
(DEFERRED). ONE verb = both writes atomically: INSERT form_versions
(version = MAX+1, serialised by a tx-scoped advisory lock on form_id, salt 71) AND UPDATE forms.current_version,
on ctx.tx. { formId, schema, title, description?, status, changelog?, createdById? }. Mints fver_.
Call POST /v/form.version.create with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.version.create.
Createform.watch.create
| Property | Value |
|---|---|
| Capability | form.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | Yes |
| Egress | None - in-box (pure Postgres, no outbound call) |
Idempotent INSERT … ON CONFLICT (form_id, user_id) DO NOTHING.
The form is fenced within the workspace. { formId }; userId = acting user.
Call POST /v/form.watch.create with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.watch.create.
Deleteform.watch.delete
| Property | Value |
|---|---|
| Capability | form.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | Yes |
| Egress | None - in-box (pure Postgres, no outbound call) |
DELETE by (form_id, acting user, workspace); idempotent. { formId }.
Call POST /v/form.watch.delete with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool form.watch.delete.