Blueprint authoring
A blueprint is one JSON file that packages an application out of platform components: its tables, the automation around them, the screens people use, and the data it ships with. Install the file and the platform builds every piece in one pass. This guide covers how the file is structured, the order you author it in, and how to validate and install it. It uses a small demo application to show the shape; for the components a blueprint assembles, read Platform first.
Goal
Author a blueprint that installs cleanly: correct structure, valid references, and data that matches its own choices.
Prerequisites
platform.tables.writeto create tables, plus the write capability for each surface the blueprint declares (platform.rules.write,platform.workflows.write,platform.approvals.write, and so on).- A JSON editor. A blueprint is hand-authored or generated, then installed through
install_app_blueprint. Connected over MCP as a Builder, you can also assemble the same application one object at a time through each page’s own tool (create_table,create_role, and so on), trying each piece before moving to the next, instead of authoring the file directly.
The file at a glance
A blueprint declares these top-level keys. Empty arrays are fine for sections you do not use.
{ "name": "Acme Requests", "description": "A simple request application.", "category": "custom", "$schema": "https://flowlakes.com/blueprint/v2.0", "schema_version": "2.0.0", "system_tables": ["fl_tasks", "users", "groups"], "metadata": { "name": "Acme Requests", "slug": "acme-requests", "description": "A simple request application.", "category": "custom", "version": "1.0.0", "tags": ["requests"], "author": "Acme", "organization": "Acme", "license": "proprietary", "is_public": false, "visibility": "private", "readme": "A demo application that captures and tracks requests." }, "choices": [], "tables": [], "relationships": [], "rules": [], "workflows": [], "notifications": [], "approvals": [], "dashboards": [], "navigation": { "modules": [] }, "roles": [], "forms": [], "service_hub_items": [], "seed_data": {}, "demo_data": {}}name, description, and category appear both at the top level and inside metadata;
keep them identical. The system_tables array lists the real platform tables your blueprint
reaches (for example fl_tasks, users, groups). Reference those directly. Do not
fabricate a parallel copy of a platform table.
Steps
1. Author the data layer
Define choices first, because tables reference them. Each choice value carries a value,
a label, and a sort_order; lifecycle status values also carry a color and their
allowed_transitions. A field points at a choice set instead of inlining its own options.
Then define tables. Classify each table honestly:
- Reference or configuration tables. Lookup data an admin maintains (priorities, categories). These get seed data.
- Process tables. The work items people act on (a request, a task). A process table
sets
task_enabled: trueand a uniquenumber_prefix. That is what gives it a record number, an activity timeline, and SLA behavior.
A process table’s lifecycle is its inherited state column, never a second field the
draft invents. Declare the table’s own lifecycle values as choices for field name
state (with allowed_transitions if the table needs a state machine), and set
default_state to one of those same values. An approval policy’s outcome
(on_approved_state, on_rejected_state) writes state directly, so its value must be
one of the table’s declared state choices too. Do not add a second status field
alongside state: a policy that writes state while an action rule watches a different
field never fires, and the record ends up with two lifecycles that disagree.
A process table also declares the number field the prefix fills. Validation refuses a
task_enabled table that carries a number_prefix and no number field, because the
platform generates the number at insert time and has nowhere to write it:
{ "field_name": "number", "field_type": "string", "field_length": 50, "is_indexed": true, "ui_config": { "readonly": true }}A process table can inherit shared behavior by naming fl_tasks as its parent, so common
fields and task automation come for free. See Tables and records.
Add relationships for reference fields between tables. A field that points at a person
uses a reference to users; a field that points at a team references groups.
2. Author the logic layer
Add rules, workflows, and approvals.
- Rules are trigger-condition-action logic on one table. Give each rule an explicit execution order so calculation runs before validation runs before action. Rule conditions and workflow branch conditions must use supported operators only. An unsupported operator is rejected at validation time, which is deliberate: at runtime an unknown operator would silently mis-fire. See Operators.
- Workflows run multi-step processes asynchronously off a trigger. See Workflows.
- Approvals are declared as approval policies that self-trigger on their own target
tables and conditions. Do not build a custom approval table. Declare
trigger_on_create,trigger_on_updateandtrigger_on_field_changeexplicitly: a condition whose value is reached by an UPDATE (not supplied at create) needstrigger_on_update: trueor that field named intrigger_on_field_change, or the policy never routes. See Approvals, Triggers.
Script bodies inside a rule or a workflow step follow The scripting sandbox.
3. Author the presentation layer
Add dashboards, notifications, navigation, and roles.
- Dashboards lay out on a 10-column grid where every row sums to exactly 10. KPI cards are height 2; charts, tables, and queues are height 4. Fill the grid with no trailing gaps.
- Notifications cover the lifecycle events that matter, with
{{field_name}}placeholders in the subject and body. See Notifications. A notification’stable_namemust name a table this package actually ships or that the tenant already has; validation refuses one that names no real table, rather than installing a rule that silently never fires. - Navigation groups the app into modules; the first item in a module is its dashboard. Icons belong on navigation modules and menu items.
- Roles grant real platform capabilities and per-table permissions. Declare at least an admin, a manager, and a user role. See Identity and access.
4. Author the data the blueprint ships with
Two data sections, with different lifetimes:
seed_data. Always installed. Put reference and configuration rows here (the priorities, categories, and calendars the app needs to function). Seed values must match the choice values you defined exactly. Never seed process records here.demo_data. Opt-in. Put sample process records and their supporting demo groups, services, and configuration items here. An installer chooses whether to include it with the Include demo data checkbox in Browse Apps. Insidedemo_datayou can reference the installing user with$current_user, and cross-reference a row you are seeding with{"$ref": "alias"}after declaring that alias with_ref. Rows seed order-independently, so never rely on key or table order.
5. Validate, then install
Install a complete file directly with install_app_blueprint, which validates the whole
file through the same validator before installing anything.
Validation checks that the JSON is well-formed, every choice-set reference resolves, every reference-table value exists, and each task-enabled table has a unique number prefix.
6. Update an installed application
Each component the platform installs is found again, on every later update, by its own
stable key, not by its display name: a table by its table_name, a workflow by its name,
and so on (the full list is in Blueprint standard, Keys).
Renaming a component’s display copy, such as rewording a workflow’s name, is not a rename to
the platform unless you say so: declare the component’s own "key" once, and its display
fields are then free to change on every later release, or declare "renamed_from": ["Old name"] on the renamed component so the installer updates the tenant’s existing row instead of
leaving it behind and creating a second one. Dropping a component from the package retires it
everywhere this tenant has it installed, by key.
Editing the file changes what NEW installs receive. A tenant that already installed the app
receives nothing until someone applies the update from Marketplace, so bump
metadata.version and apply the update in every tenant that should get the change.
Clicking Update shows what the update would do before you confirm it:
- Summary. The components the update would add or change, or “No changes detected”.
- Breaking changes. Anything that alters an existing table or field. An update with breaking changes is flagged rather than blocked.
- Installed record. Where the platform’s record of what this tenant installed comes from. “Recorded for this app” gives an exact list of changes. “Not recorded” means the changes cannot be computed, and the dialog offers to re-apply everything the blueprint declares instead. That re-apply updates existing components in place and records a baseline, so the next update is exact.
After you confirm, the result lists what was applied and what was not. An update that leaves anything unapplied reports a failure and does not update the record of what is installed, so applying again retries exactly what failed.
Uninstalling reports the same way. Each removal step runs independently, and any step that leaves something behind is named in the result. The application is not fully removed until that list is empty.
Building one object at a time over MCP
An AI client connected over MCP as a Builder does not have to author the whole file at
once. It works the way an admin does: read what exists (list_tables, get_schema, and
the other list_*/get_* tools), build one object at a time through that object’s own
tool (create_table, create_role, create_business_rule, create_workflow,
create_approval_policy, create_notification_rule, create_dashboard,
create_menu_item, create_catalog_item, and so on), and try each piece before moving
to the next. Every write runs through the same validator install_app_blueprint runs,
so a Builder never ends up with something the installer would have refused.
Verification
You know it worked when:
- After install, the app’s tables, navigation module, and dashboards appear in the tenant.
- Creating a record in a process table assigns it a number and a starting state.
- If you installed with demo data, the sample records are present; without it, only the reference data is.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Validation rejects an operator | A condition uses an unsupported operator | Use only the supported operators |
| Install fails on a duplicate prefix | Two task-enabled tables share a number_prefix | Give each process table a unique prefix |
| Validation rejects a task-enabled table | The table has a number_prefix and no number field | Declare the number field shown above |
| An update reports “No changes detected” after a file edit | The tenant’s installed record is a version snapshot that no longer matches the file | Bump metadata.version, or re-apply everything from the update dialog |
| An uninstalled app is still partly present | A removal step left something behind | Read the named steps in the uninstall result and clear them before reinstalling |
| A choice-driven field is empty | Seed value does not match a defined choice value | Align seed data with the choices exactly |
| Demo records missing after install | Demo data was not requested | Re-install with the Include demo data checkbox |
Validation rejects a cancelled state | The table has its own cancel_reason field, but the cancelled choice row has no requires entry for it | Add {"field": "cancel_reason"} to that state’s requires list |
| Validation rejects a string field | Its description reads like an enumerated option list (one of:, options:, allowed values:) | Declare it as a choice field with a matching choices[] entry instead of describing the options in prose |
| Validation rejects a top-level key | The key is not one of the declared sections or the base-object keys (name, description, category, $schema, schema_version, metadata, depends_on) | Remove it, or correct the spelling to the section the installer actually reads |
Related
- Blueprint standard: the full specification this guide walks through, with every required section and valid enumeration.
- Platform: the primitives a blueprint assembles.
- The scripting sandbox: the environment for script bodies in rules and workflows.
- Operators: the condition and filter vocabulary validation enforces.
- Glossary: every platform term used above.