---
title: "Build pages from feature parts"
section: "Builder Guide"
canonical_url: "https://support.buildwithhq.com/builder-guide/feature-parts.html"
reviewed_at: "2026-09-14"
authority: Official
---

# 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.

## In brief

- **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.

| Feature | Block type | Parts (`props.part`) | Can follow a record |
| --- | --- | --- | --- |
| Contacts | `module.contacts` | header, filters, notices, list, pager, detail, form | Yes |
| Work Orders | `module.work-orders` | header, filters, notices, list, pager, detail, form, payments | Yes |
| Reservations | `module.reservations` | header, filters, notices, list, pager, detail, form, payments | Yes |
| Calendar | `module.calendar` | header, views, filters, notices, list, detail, form | Yes |
| Files | `module.files` | header, filters, notices, list, pager, detail, form, upload | Yes |
| Company News | `module.company-news` | header, filters, notices, list, pager, detail, form | No |
| Web Links | `module.web-links` | header, filters, notices, list, pager, detail, form | No |
| Favorites | `module.favorites` | header, filters, notices, list, pager, detail | No |
| Knowledge Articles | `module.knowledge-articles` | header, notices, health, review, filters, list, pager, detail, form | No |
| Checklists & Sign-offs | `module.checklists-signoffs` | header, notices, filters, list, pager, detail, form, templates | No |
| Chat Rooms | `module.chat-rooms` | header, filters, notices, list, pager, detail, form, thread, members | No |
| Discussion Boards | `module.discussion-boards` | header, filters, notices, list, pager, detail, form, topics, thread | No |
| Conversations | `module.conversations` | header, filters, notices, list, pager, detail, thread, form | No |
| Dynamic Records | `module.dynamic-records` | header, filters, notices, list, pager, detail, form | No |

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](raw-json-mode.html)  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.

## Link types

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. [View the canonical guide](https://support.buildwithhq.com/builder-guide/feature-parts.html).
