Support/Builder Guide/Build pages from feature parts

Builder Guide

Build pages from feature parts

Split a feature into pieces you place anywhere, put one feature inside another, and build complete solutions such as a customer page with its jobs, appointments and files.

Open raw .md
Where
Design, in Puck or advanced JSON
Applies to
14 features
Data access
Unchanged: every piece uses the feature's own permission checks

Every feature block on a page can be split into parts: its header, filters, list, detail, form and other pieces. You place the parts wherever your design needs them, and one feature can sit inside another. That is how you build a complete solution, for example a customer page for a home-services business that shows the customer, their jobs, appointments, files and events together.

Workspaces and parts

A feature block is either its workspace or one of its parts, chosen by the block property part. The workspace loads the data, saves changes and keeps every safety check. Parts only show the piece the workspace hands them, so splitting a screen never splits its data or its permissions.

  • Add the feature block once, with no part. That is the workspace.
  • Inside it, through any layout blocks you like, add the same feature block again with part set, for example "list" or "detail".
  • If you place no parts, the workspace shows its built-in layout exactly as before.
FeatureBlock typeParts (props.part)Can follow a record
Contactsmodule.contactsheader, filters, notices, list, pager, detail, formYes
Work Ordersmodule.work-ordersheader, filters, notices, list, pager, detail, form, paymentsYes
Reservationsmodule.reservationsheader, filters, notices, list, pager, detail, form, paymentsYes
Calendarmodule.calendarheader, views, filters, notices, list, detail, formYes
Filesmodule.filesheader, filters, notices, list, pager, detail, form, uploadYes
Company Newsmodule.company-newsheader, filters, notices, list, pager, detail, formNo
Web Linksmodule.web-linksheader, filters, notices, list, pager, detail, formNo
Favoritesmodule.favoritesheader, filters, notices, list, pager, detailNo
Knowledge Articlesmodule.knowledge-articlesheader, notices, health, review, filters, list, pager, detail, formNo
Checklists & Sign-offsmodule.checklists-signoffsheader, notices, filters, list, pager, detail, form, templatesNo
Chat Roomsmodule.chat-roomsheader, filters, notices, list, pager, detail, form, thread, membersNo
Discussion Boardsmodule.discussion-boardsheader, filters, notices, list, pager, detail, form, topics, threadNo
Conversationsmodule.conversationsheader, filters, notices, list, pager, detail, thread, formNo
Dynamic Recordsmodule.dynamic-recordsheader, filters, notices, list, pager, detail, formNo

A few placement rules keep every action reachable:

  • A form opens in the form part if you placed one, otherwise in detail, otherwise in list.
  • Messages and errors appear at the top of the workspace when you don't place a notices part.
  • Feature-specific parts you leave out (for example thread or members) appear inside detail.
  • A part placed outside its feature's workspace explains where it belongs and loads nothing.
  • An unknown part value is treated as the workspace.

Arrange parts in a grid

Use a Container block (core.container) with layout set to "grid" and gridMode set to "twelve-column". Give each child a responsive span per screen size, for example 4 columns for the list and 8 for the detail on desktop, and 12 on tablet and phone so they stack.

Tip

Without layout: "grid" and gridMode: "twelve-column" a container stacks its children, and the spans have no effect.

Put one feature inside another

Each workspace shares the record selected in it with the rest of the page under a context key. By default the key is the feature's own name (contacts, work-orders and so on); set contextKey to name it yourself, for example "customer".

Another feature follows that record when you set its recordFrom to the same key. It then lists only the records linked to the selected one:

  • Work Orders: jobs whose customer is the selected contact, or that are linked to the selected record.
  • Reservations: reservations booked for the selected contact, or linked to it.
  • Files: files attached to the selected record, or linked to it.
  • Calendar: events linked to the selected record.
  • Contacts: the customer of a selected work order or the contact of a selected reservation, or contacts linked to it.

Until something is selected, a following workspace loads nothing and says what to select. Set followEmptyText to use your own wording. When the selection changes, it reloads from scratch, so nothing from the previous record carries over.

Note

Following never widens access. The person must be able to see the selected record; if they can't, or it was deleted, the following list is empty. Every row still goes through the feature's own permission checks.

Only the workspace that drives the page reads and writes the page address's ?record= deep link. Following workspaces ignore it, so opening a shared link to a customer never tries to open that customer as a job or a file.

Example: a home-services customer page

A customer list and detail side by side, with the selected customer's jobs, appointments, files and events below. Paste it into advanced JSON mode as a starting point.

{
  "blocks": [
    { "_id": "customers", "_type": "module.contacts",
      "props": { "title": "Customers", "singular": "customer", "contextKey": "customer", "pageSize": 10 },
      "children": [
        { "_id": "customers-header",  "_type": "module.contacts", "props": { "part": "header" },  "children": [] },
        { "_id": "customers-filters", "_type": "module.contacts", "props": { "part": "filters" }, "children": [] },
        { "_id": "customers-row", "_type": "core.container",
          "props": { "layout": "grid", "gridMode": "twelve-column" },
          "children": [
            { "_id": "customers-list", "_type": "module.contacts",
              "props": { "part": "list", "responsive": { "desktop": { "span": 4 }, "tablet": { "span": 12 }, "mobile": { "span": 12 } } },
              "children": [] },
            { "_id": "customers-detail", "_type": "module.contacts",
              "props": { "part": "detail", "responsive": { "desktop": { "span": 8 }, "tablet": { "span": 12 }, "mobile": { "span": 12 } } },
              "children": [] } ] },
        { "_id": "customers-pager", "_type": "module.contacts", "props": { "part": "pager" }, "children": [] } ] },
    { "_id": "jobs", "_type": "module.work-orders",
      "props": { "title": "Jobs", "recordFrom": "customer", "contextKey": "job",
                 "followEmptyText": "Select a customer to see their jobs." }, "children": [] },
    { "_id": "appointments", "_type": "module.reservations",
      "props": { "title": "Appointments", "recordFrom": "customer",
                 "followEmptyText": "Select a customer to see their appointments." }, "children": [] },
    { "_id": "customer-files", "_type": "module.files",
      "props": { "recordFrom": "customer", "contextKey": "customer-file" }, "children": [] },
    { "_id": "customer-events", "_type": "module.calendar",
      "props": { "recordFrom": "customer", "contextKey": "customer-event" }, "children": [] }
  ]
}

Create in context

When a workspace follows a contact, new records start linked to that contact:

  • New work order starts with the selected contact as its customer.
  • New reservation books for the selected contact; the form keeps the email used for follow-ups and skips the new-contact fields.

A link type names how two records relate, for example Jobs from a customer and Customer back from a job. The Related panel uses link types when people link records by hand, and the app enforces each type's rules on every new link:

  • Features: a type can be limited to links from one feature to another, for example from Contacts to Work Orders. A link between other kinds of records is refused.
  • One only: a type can allow a single linked record per record. A second link of that type is refused until the first is removed; repeating the same link is accepted.
  • Active: a turned-off type is refused like an unknown one and is hidden from people choosing a link.

The built-in link types (for example the customer of a work order and the contact of a reservation) can't be changed. Links a feature creates itself, such as a new work order's customer, keep working whatever rules you set. Changing a type's rules applies to links made from then on; existing links are kept.

Rename features

Each app can give a feature its own name, singular and plural, for example Customer and Customers instead of Contact and Contacts, or Student and Students for a school band app. Only the words on screen change; the feature, its data and its permissions stay the same. Clearing both names returns the feature to its built-in name. A block's own title still wins on that block.

Set both in Design, on the Links & names tab. Built-in link types show there read-only; if someone else changed a link type since you opened it, saving asks you to reload first.

Important

Defining link types and renaming features is limited to people who can design the app. Everyone signed in sees the names and the active link types.

Do the same through the Developer API

  • Build and publish these pages with the app design routes under /v1/apps/{saasAppId}/design, using your builder keys, exactly as in the designer.
  • The Contacts, Work Orders, Reservations, Calendar and Files list operations take an optional relatedRecordId query parameter with the same rules as following a record: only linked rows, and an empty list when the caller can't read that record. An all-zero ID is refused with 400. Contacts reports hasMore instead of an exact total for a linked list.
  • Read and change link types and feature names with your builder keys: GET /v1/apps/{saasAppId}/design/link-types returns every link type and every feature with its app name; PUT /v1/apps/{saasAppId}/design/link-types/{relationKey} creates or changes a link type (send the updatedUtc you were shown when changing one); PUT /v1/apps/{saasAppId}/design/vocabulary/{moduleKey} sets or clears a feature's names. Saves change the live app, so they need a live key. Inside the app, the same routes are /api/runtime/design/link-types and /api/runtime/design/vocabulary/{moduleKey}, and any signed-in user reads the active link types and names from GET /api/runtime/vocabulary.
  • Bad input is 400; a change to a link type that changed since it was shown, or to a built-in type, is 409.
  • Link records with the record graph relation operations; the link type rules above apply. Linking the wrong kinds of records is 422; a second link of a one-only type is 409.

Not supported yet

  • Only Contacts, Work Orders, Reservations, Calendar and Files can follow a record. The other features can be split into parts but don't follow.
  • Checklists & Sign-offs shares the selected checklist run, not a record, so other features can't follow a checklist yet.
  • Part, context key and follow settings are edited as component properties (or in advanced JSON); dedicated pickers in the designer are still to come.

Capability review: 2026-09-14. See current features and setup requirements, the generated API Map and first-class module inventory.