Blueprint Reference
Blueprints are TOML files that declaratively define your application. This page covers the core data, page, UI, authentication, and integration sections. Domain behavior has a dedicated Domain Commands reference, while Workflows and Skills cover their respective sections in depth.
Project
Section titled “Project”Every blueprint starts with a version and project metadata:
version = "0.6.0"
[project]name = "My App"version = "1.0.0"description = "An internal tool"
[project.runtime]min_version = "0.6.0"Composing multiple TOML files
Section titled “Composing multiple TOML files”Large applications can split a Blueprint into explicitly imported TOML fragments. Put imports at the top of the root file, before any table declaration:
imports = [ "./foundation/design-system.toml", "./domain/requests/module.toml",]
version = "0.6.0"
[project]name = "My App"version = "1.0.0"
[project.runtime]min_version = "0.6.0"Fragments can import other fragments. Each path is resolved relative to the file containing it, so a directory can own its composition without registering names globally:
imports = ["./entities.toml", "./commands.toml", "./workflows.toml"]Only the root file defines version and project. Imported files can use any .toml filename and contain any other Blueprint sections. Runtime names remain application-global: two imported files cannot both define the same entity, command, workflow, page, service, skill, or plugin.
Imports must name explicit relative .toml files. Globs, absolute paths, files outside the root Blueprint directory, cycles, and ambiguous singleton configuration are rejected. Importing the same canonical file through multiple branches incorporates it once, which allows a shared design-system.toml to be reused safely.
Entities
Section titled “Entities”Entities define your data model. The runtime creates database tables automatically.
[entity.Post]fields = [ { name = "id", type = "ULID", primary_key = true }, { name = "title", type = "Text", required = true }, { name = "body", type = "LongText", required = true }, { name = "status", type = "Enum", values = ["draft", "published"], default = "draft" }, { name = "authorId", type = "Ref", ref = "User.id", index = true }, { name = "createdAt", type = "DateTime", default = "now" }]Field Types
Section titled “Field Types”| Type | Description |
|---|---|
ULID | Primary key, auto-generated unique identifier |
UUID | UUID identifier |
Text | Short text (VARCHAR) |
LongText | Long text (TEXT) |
Email | Email address with validation |
Integer | Whole numbers |
Float | Decimal numbers |
Boolean | true/false |
DateTime | Date and time (shown with its time zone) |
Date | Date only |
JSON | JSON data |
Enum | Enumerated values (use values = [...]) |
Ref | Foreign key reference (use ref = "Entity.field") |
Field Options
Section titled “Field Options”| Option | Type | Description |
|---|---|---|
primary_key | boolean | Mark as primary key |
unique | boolean | Enforce uniqueness |
index | boolean | Create database index |
required | boolean | Cannot be null |
nullable | boolean | Explicitly allow null |
default | any | Default value ("now" for DateTime) |
values | string[] | Allowed values for Enum type |
ref | string | Foreign key target for Ref type |
write | "command-only" | Reject generic writes to a lifecycle field |
commands | string[] | Commands allowed to write a command-only field |
Indexes and Entity Protection
Section titled “Indexes and Entity Protection”Define compound or named indexes with [[entity.Post.indexes]]:
[[entity.Post.indexes]]fields = ["authorId", "status"]name = "post_author_status"
[[entity.Post.indexes]]fields = ["slug"]unique = trueProtect several fields with one command allowlist:
[entity.Post.protection]fields = ["status", "publishedAt"]commands = ["PublishPost", "UnpublishPost"]See Domain Commands for command-only mutation behavior.
Relations
Section titled “Relations”[entity.User.relations]posts = { type = "hasMany", entity = "Post", foreign_key = "authorId" }
[entity.Post.relations]author = { type = "belongsTo", entity = "User", foreign_key = "authorId" }Relation types: hasMany, hasOne, belongsTo, manyToMany (requires through).
Access Control
Section titled “Access Control”Define who can read, create, update, or delete records:
# Simple boolean[entity.Post.access]read = truecreate = trueupdate = { authorId = "$currentUser.id" }delete = { "$currentUser.role" = "admin" }Use $currentUser to reference the logged-in user’s fields. Combine conditions with or/and:
[entity.Post.access]read = { or = [{ status = "published" }, { authorId = "$currentUser.id" }] }Field-level access:
[entity.User]fields = [ { name = "email", type = "Email", access = { read = true, write = { "$currentUser.role" = "admin" } } }]Pages bind URL paths to layouts and queries.
Set live = true on a page to keep its server-rendered main content synchronized with committed mutations on Node and Workers. See Live Mode for browser behavior and limitations.
Pages can also mount interactive widgets directly under [page."/path".widget]. For the current widget types and examples, see Widgets, Board Widget, and Lookup Widget.
Layout Types
Section titled “Layout Types”| Layout | Description |
|---|---|
list | Table view of query results |
detail | Single record display |
form | Create/update/delete forms |
dashboard | Widgets and summary cards |
auth | Authentication pages (sign-in) |
List Page
Section titled “List Page”[page."/posts"]title = "All Posts"auth = "none"layout = "list"
[page."/posts".query.posts]entity = "Post"where = { status = "published" }orderBy = { createdAt = "desc" }limit = 20include = ["author"]Detail Page
Section titled “Detail Page”[page."/posts/:id"]title = "Post Detail"auth = "none"layout = "detail"
[page."/posts/:id".query.post]entity = "Post"where = { id = "$params.id" }include = ["author"]How values are displayed
Section titled “How values are displayed”Detail pages and related lists show each field according to its type. All values are HTML-escaped. Line breaks in LongText
and JSON are kept on detail pages and in activity feeds; table cells show values on one line.
| Type | Display |
|---|---|
LongText | Line breaks are kept (detail pages) and long lines wrap |
JSON | Pretty-printed (monospaced on detail pages). Keys that look like technical identifiers (for example id) are left out |
DateTime | Date, time and time zone, such as 10/8/2026, 2:13:42 AM UTC |
Date | Date only |
Boolean | A check or cross mark |
| empty | - |
The time zone is the server’s: Cloudflare Workers always render in UTC, and the Node runtime uses the process’s zone. The zone is named so a reader in another zone is not misled.
Queries other than the page’s main record appear as related sections below it. Zebric chooses how to present each one from the entity’s name, which is worth knowing when a section does not look the way you expect:
| Entity name contains | Presented as |
|---|---|
task | A checklist |
milestone or timeline | A timeline |
activity or event | An activity feed |
| anything else | A table |
An activity feed shows, for each record, the first of title, summary, action, kind, type or name as its heading;
the first of detail, description or message beneath it (line breaks kept); and the first of timestamp or createdAt as
its time. A record with none of these fields shows the word “Event”.
Dashboard Page
Section titled “Dashboard Page”[page."/"]title = "Dashboard"auth = "required"layout = "dashboard"
[page."/".query.recentPosts]entity = "Post"orderBy = { createdAt = "desc" }limit = 5
[page."/".query.totalUsers]entity = "User"Query Options
Section titled “Query Options”| Option | Description |
|---|---|
entity | Entity to query |
where | Filter conditions (use $params.id for URL params) |
orderBy | Sort order ({ field = "asc" } or "desc") |
limit | Maximum number of results |
offset | Skip N results |
include | Related entities to include |
Widgets
Section titled “Widgets”Widgets are page-level interactive views that live alongside standard page configuration:
[page."/customers"]title = "Customer Search"
[page."/customers".widget]kind = "lookup"entity = "Customer"search = ["lastName", "firstName", "email"]Use widgets when a page should behave more like a tool than a traditional CRUD screen. Zebric currently supports:
boardfor Kanban-style issue and workflow pageslookupfor standalone search-and-select pages
For full widget configuration and examples, see Widgets, Board Widget, and Lookup Widget.
Forms handle record creation and updates.
[page."/posts/new"]title = "New Post"auth = "required"layout = "form"
[page."/posts/new".form]entity = "Post"method = "create"
[[page."/posts/new".form.fields]]name = "title"type = "text"label = "Title"required = true
[[page."/posts/new".form.fields]]name = "category"type = "select"label = "Category"options = [ { value = "general", label = "General" }, { value = "finance", label = "Finance" }]
[[page."/posts/new".form.fields]]name = "body"type = "textarea"label = "Content"rows = 10required = true
[page."/posts/new".form.onSuccess]redirect = "/posts/{id}"message = "Post created!"Form Field Types
Section titled “Form Field Types”text, textarea, email, password, number, select, checkbox, radio, file, date, datetime, hidden
Use hidden for technical values that must be submitted with a form but should
not be presented to the user, such as a parent record identifier.
Form Field Options
Section titled “Form Field Options”| Option | Description |
|---|---|
label | Display label |
placeholder | Placeholder text |
required | Field is required |
default | Default value |
options | Choices for select/radio (string array or { value, label } objects) |
rows | Number of rows for textarea |
pattern | Regex validation pattern |
error_message | Custom error message |
min / max | Number range constraints |
accept | Accepted file types for file input |
Action Bar
Section titled “Action Bar”Detail pages can include an action bar with workflow triggers and a status badge. They render domain commands for the primary entity when the command’s policy and availableWhen checks pass, even when no explicit action-bar block is present:
[page."/issues/:id"]title = "Issue Detail"layout = "detail"
[page."/issues/:id".actionBar]statusField = "status"
[[page."/issues/:id".actionBar.actions]]label = "Set Status"workflow = "SetIssueStatus"style = "primary"
[[page."/issues/:id".actionBar.actions]]label = "Request Approval"workflow = "RequestApprovalIfNeeded"style = "secondary"visibleWhen = { status = ["draft", "changes_requested"] }Command actions do not need to be repeated in actionBar.actions; define their label, input, confirmation prompt, and style on the command. See Generated UI.
Workflow and link actions support visibleWhen and enabledWhen record conditions, along with confirm, payload, redirect, successMessage, and errorMessage. Put authoritative state checks in the workflow or command as well; UI visibility is not an authorization boundary.
Layout Slots
Section titled “Layout Slots”Customize layout rendering with named slots:
| Layout | Available Slots |
|---|---|
list | list.header, list.body, list.empty |
detail | detail.main, detail.related |
form | form.form |
dashboard | dashboard.widgets |
UI Configuration
Section titled “UI Configuration”[ui]render_mode = "server"theme = "default"progressive_enhancement = "none" # "none", "alpine", or "htmx"view_transitions = true
[ui.tailwind]primary_color = "#3B82F6"Theming
Section titled “Theming”Zazzle selects visual styles through [design_system]. The default is modern;
classic, friendly, and minimal are also built in, each with a dark palette.
[design_system]name = "acme"extends = "friendly"css = ["/styles/acme.css"]
[design_system.tokens]color-primary = "#5636cc"color-error = "#a12622"color-info = "#24548a"font-family-heading = "Georgia, serif"Token names become --zb-* CSS custom properties. Generated details, related
feeds, action bars, status badges, feedback, and form controls use semantic
classes so their colors follow the selected system. Success uses color-success,
warning uses color-warning, and errors and destructive actions use color-error.
Informational feedback uses color-info. Status badges and feedback use
surface-card as their background.
The navigation color-mode control cycles auto/light/dark and saves the visitor’s
choice locally. Custom token overrides apply to the light palette; inherited dark
colors come from the base system. Use CSS scoped to
html[data-zebric-design-system="acme"][data-zebric-resolved-color-mode="dark"]
to customize dark colors.
The renderer ships two Theme objects — defaultTheme (light) and darkTheme — exported from @zebric/runtime-core. The theme field above is accepted by the schema but not yet wired up to select between them automatically; applying a different theme currently requires passing a Theme object to the renderer programmatically rather than by name in the blueprint.
Table interactions and navigation
Section titled “Table interactions and navigation”Global UX defaults can be overridden on individual pages. Fields are merged
within interaction, data, and form; changing one page setting retains the
other global defaults. Matching entries in ux.patterns supply pattern defaults
between global and page configuration.
[ux.navigation]model = "sidebar"primary = ["Tasks", "Activity"]
[ux.responsive]collapse_sidebar = true
[page."/tasks".ux.interaction]selection = "multi"row_click = "select"
[page."/tasks".ux.data]filters = "top-bar"column_config = truepagination = "client"Sidebar navigation occupies a left column on desktop and collapses behind a
Menu button below 48rem. Set collapse_sidebar = false to keep its links visible
on small screens.
Table selection supports single or multi, including keyboard row selection
when row_click = "select". Multi-selection includes a select-visible-rows
checkbox; selection is retained while filtering or changing client pages. The
table emits a bubbling zebric:selection-change event with detail.ids. Selection
alone does not run bulk mutations. row_click = "edit" navigates to the entity’s
update page when one exists.
filters adds a text filter over the loaded rows, positioned in a top bar or
sidebar; it does not query the database. column_config = true adds column
visibility controls. Client pagination shows 25 loaded rows at a time. Controls
are scoped to each table and initialize again after Live Mode replaces content.
For pagination = "server", the main query on a list page uses its limit as
the page size (default 25, maximum 250). Previous/Next links use zb_page and
retain other query parameters. The runtime fetches one lookahead row to decide
whether Next should be available, then removes it from the returned data.
Related queries keep their configured limits and offsets. Set orderBy on the
main query for stable ordering across pages. Filtering in this mode searches the
current page’s rows. JSON page responses include pagination metadata.
Progressive Enhancement
Section titled “Progressive Enhancement”Any value other than "none" enables client-side form enhancement: forms render with a data-enhance attribute, and a small inline script intercepts submits, does an AJAX fetch with CSRF-token forwarding, shows a loading state on the submit button, and redirects or displays inline field errors based on the JSON response — no full page reload. The "alpine" and "htmx" values are accepted by the schema but don’t currently load those libraries; they behave the same as any non-"none" value.
View Transitions
Section titled “View Transitions”view_transitions = true adds a <meta name="view-transition"> tag and @view-transition CSS so supporting browsers animate page navigations.
Content Negotiation
Section titled “Content Negotiation”Every page responds with HTML by default, or JSON if the request’s Accept header includes application/json without also including text/html:
curl http://localhost:3000/posts # HTMLcurl -H "Accept: application/json" http://localhost:3000/posts # JSONAccessibility
Section titled “Accessibility”Rendered pages include WCAG-oriented features: a skip-to-content link, aria-label/aria-current on navigation, table <caption> and scope="col" headers, aria-describedby/aria-invalid/role="alert" on form validation errors, and an sr-only utility class for screen-reader-only content.
Authentication
Section titled “Authentication”[auth]providers = ["email"]trustedOrigins = ["http://localhost:3000"]
[[auth.apiKeys]]name = "my-agent"keyEnv = "MY_API_KEY"agentId = "content-agent"credentialId = "content-agent-production"displayName = "Content Agent"roles = ["editor"]scopes = ["entity.post.list", "posts.publish"]API keys can authenticate entity, skill, workflow, command, job, and audit routes. See Security & Authentication for identity and scope behavior.
Domain Commands
Section titled “Domain Commands”Commands define guarded business transitions shared by UI, HTTP, agents, and workflows:
[command.PublishPost]entity = "Post"label = "Publish"policy = "actor.roles contains 'editor'"availableWhen = "record.status == 'draft'"mutations = { status = "published", publishedAt = "now" }scopes = ["posts.publish"]See Domain Commands for typed input, protected fields, handlers, generated UI, workflow steps, and HTTP invocation.
External Services
Section titled “External Services”Services declare typed operations supplied by a loaded plugin:
[service.crm]description = "Customer system"plugin = "crm-plugin"
[service.crm.operations.lookupCustomer]description = "Find a customer by email."
[service.crm.operations.lookupCustomer.input.email]type = "Email"required = true
[service.crm.operations.lookupCustomer.result]type = "Object"fields = { id = { type = "Text", required = true }, name = { type = "Text" } }allowUnknown = falseEach service requires at least one operation. Input and result schemas accept entity scalar types plus nested Object and Array values. Object requires fields; Array requires items. The optional transform map projects a validated external result before returning it to the caller.
Notifications
Section titled “Notifications”[notifications]default = "console"
[[notifications.adapters]]name = "console"type = "console"
[[notifications.adapters]]name = "slack_ops"type = "slack"[notifications.adapters.config]botTokenEnv = "SLACK_BOT_TOKEN"defaultChannel = "#ops"Adapter types: console, slack, email.
Testing Blueprints
Section titled “Testing Blueprints”Use the framework stories package to test your blueprints:
pnpm --filter @zebric/framework-stories testSee the examples/ directory for complete blueprint files.