> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sapt.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Project templates & the bundle

> What lives inside a project template's bundle — the portable config that gets stamped onto a new client project — plus the snapshot, apply, and load endpoints.

A **project template** is a saved blueprint you stamp onto new projects. Its one meaningful field is the **`bundle`**: a single JSON object holding pure, portable configuration. When you apply a template, Sapt replays that bundle into a freshly-created sub-project.

This page documents what's *inside* the bundle. For the end-to-end onboarding flow (apply → connect integrations → invite), see [Building a client onboarding funnel](/docs/mintlify/api/onboarding-funnels).

<Note>
  **Why the reference shows `bundle` as an open object.** The bundle is a deeply-nested structure (it embeds workflow conditions, CRM column defs, onboarding flows, and more), so the auto-generated [Reference](/api-reference/socials/create-post) renders it as free-form JSON rather than a fixed schema. **This page is the schema** — treat it as the source of truth for the bundle's shape.
</Note>

## What the bundle is — and isn't

The bundle is **structural config only**. It carries **no project-local UUIDs**: roles are keyed by `name`, and the sidebar's per-role overlay references those same names. Apply remaps each name to the freshly-created role's id.

| In the bundle                                    | Never in the bundle                                                                    |
| ------------------------------------------------ | -------------------------------------------------------------------------------------- |
| Roles, sidebar layout, workflows                 | Integration connections & OAuth tokens                                                 |
| CRM object types, relations, column config       | API keys, credentials, any secret                                                      |
| CMS content types (and optional starter content) | Live CRM record *data* (unless `starterContent` is set)                                |
| Branding, brand-voice context, agents            | Account-specific refs (sender emails, connected-account ids) — **scrubbed** on capture |

Anything tenant-specific that *would* dangle in a fresh project is nulled during capture and reported back to you in `warnings`.

## Core sections

These five are **always present** on a bundle and are applied in this **fixed order** — later steps may reference earlier ones (workflows and object types reference spine and bundle slugs):

| # | Field             | What it holds                                                                                                                  |
| - | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| 1 | `objectTypes`     | CRM object types to create.                                                                                                    |
| 2 | `objectRelations` | Relations between those object types.                                                                                          |
| 3 | `roles`           | Project roles — `{ name, description?, permissions[] }`. Permissions are capped to what the editor holds, re-checked at apply. |
| 4 | `sidebar`         | Dashboard `sections[]` plus `perRoleHidesByRoleName` — a per-role visibility overlay keyed by role **name**.                   |
| 5 | `workflows`       | Tenant workflows — trigger + condition + actions. Loaded **paused** when loaded into an existing project.                      |

<Warning>
  Apply is **best-effort**: a failure mid-bundle is recorded and swallowed, leaving whatever earlier steps already committed. Always check the [apply report](#the-apply-report)'s `failures[]` — it never throws away the whole call for one bad entity.
</Warning>

## Extended sections

All **optional**. Each reuses the canonical schema from its owning module, so the bundle shape can't drift from the live product:

| Field            | What it holds                                                                                            |
| ---------------- | -------------------------------------------------------------------------------------------------------- |
| `crmConfig`      | Per-scope CRM column config (base / lead / customer / company / activity columns + column order).        |
| `spineFields`    | Custom field definitions added to the seeded `person` spine type.                                        |
| `contentTypes`   | CMS content types plus their declared relations.                                                         |
| `starterContent` | `{ enabled, items[] }` — live CMS content items to seed. The **only** way record data enters the bundle. |
| `branding`       | Description, tagline, keywords, colors, fonts, logos, guidelines. Asset refs are nulled on capture.      |
| `agents`         | `{ definitions[], memoryEntries[] }` — agent definitions and seed memory.                                |
| `projectContext` | The three prose blobs — `brand`, `icp`, `strategy`.                                                      |
| `onboarding`     | A client onboarding questionnaire (steps / blocks / rules).                                              |
| `variables`      | Declared per-client template variables — see below.                                                      |

## Variables (custom values)

A bundle can declare **variables** — the "custom values" pattern. Embed `{{name}}` placeholders anywhere in the bundle's strings; at apply time they're substituted with the client's values.

```json theme={null}
{
  "variables": [
    { "name": "clientName",   "label": "Client name",   "type": "string", "required": true },
    { "name": "supportEmail", "label": "Support email",  "type": "string", "example": "help@acme.com" }
  ]
}
```

| Field      | Notes                                                                              |
| ---------- | ---------------------------------------------------------------------------------- |
| `name`     | The placeholder key. Must match `^[a-zA-Z_][\w]*$` (leading letter or underscore). |
| `label`    | Human-readable name shown when filling the template.                               |
| `type`     | `string` or `number`.                                                              |
| `example`  | Optional sample value.                                                             |
| `required` | When `true`, apply gates on a value being supplied.                                |

At apply you pass `variableValues` (a `name → value` map). Anything you omit comes back in the report's `missingVariables[]` and is left as a literal `{{placeholder}}` to fill in later.

## The lifecycle

You can author a bundle by hand, or **snapshot** a live project to capture one automatically. Either way it ends up as a template you can **apply** (to a new sub-project) or **load** (into an existing one).

| Endpoint                                                  | Does                                                                                                                         |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `GET /projects/{projectId}/templates`                     | List templates saved on this project.                                                                                        |
| `GET /projects/{projectId}/templates/{templateId}`        | Fetch one template (including its full `bundle`).                                                                            |
| `POST /projects/{projectId}/templates`                    | Create a template from a bundle you author directly.                                                                         |
| `PATCH /projects/{projectId}/templates/{templateId}`      | Update name, description, or bundle. Omitted fields are untouched.                                                           |
| `DELETE /projects/{projectId}/templates/{templateId}`     | Delete a template. Already-applied sub-projects are unaffected.                                                              |
| `POST /projects/{projectId}/snapshot`                     | **Capture** this project's live config into a new template in one step.                                                      |
| `POST /projects/{projectId}/templates/{templateId}/apply` | **Apply** — create a new sub-project and stamp the template onto it.                                                         |
| `POST /projects/{projectId}/templates/{templateId}/load`  | **Load** — additively copy the template into an existing project (`targetProjectId`), skipping anything that already exists. |

### Snapshotting a live project

`POST /projects/{projectId}/snapshot` captures the project's configuration into a reusable template. **Omit `include` to capture everything**; set keys to `true` to restrict capture to those subsystems.

```bash theme={null}
curl -X POST https://api.sapt.ai/projects/$PROJECT_ID/snapshot \
  -H "Authorization: ApiKey sapt_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Agency starter",
        "include": { "roles": true, "sidebar": true, "workflows": true, "branding": true }
      }'
```

The `include` allowlist accepts: `roles`, `sidebar`, `workflows`, `objectTypes`, `objectRelations`, `crmConfig`, `spineFields`, `contentTypes`, `starterContent`, `branding`, `projectContext`, `agents`. Live CMS content items are cloned **only** when `starterContent` is included. The response returns the new `template` plus `warnings[]` — the tenant-specific values that were scrubbed.

<Note>
  You can also tokenize on capture: pass `tokenize` (a list of `{ find, var }` replacements) alongside `variables` to turn literal values in the captured bundle into `{{var}}` placeholders — so a snapshot of one client becomes a parameterized template for the next.
</Note>

### Apply vs. load

Both replay a bundle, but they differ in target and conflict behavior:

|             | **Apply**                                  | **Load**                                           |
| ----------- | ------------------------------------------ | -------------------------------------------------- |
| Target      | A **new** sub-project Sapt creates for you | An **existing** project (`targetProjectId`)        |
| On conflict | N/A — the project is empty                 | Existing assets are **skipped**, never overwritten |
| Workflows   | Active per the bundle                      | Loaded **paused**                                  |
| Permission  | `sub_projects:write` on the parent         | Admin on the target + template-read on the owner   |

## The apply report

Apply and load both return a report of what landed:

```json theme={null}
{
  "counts": {
    "objectTypes": 4, "objectRelations": 2, "roles": 3,
    "workflows": 5, "sidebarSections": 3, "perRoleHides": 1
  },
  "appliedCount": 18,
  "skipped":  [{ "entity": "role", "name": "Admin" }],
  "failures": [{ "entity": "workflow", "name": "Nightly sync", "error": "...", "code": "BAD_REQUEST" }],
  "missingVariables": ["supportEmail"]
}
```

| Field              | What it tells you                                                          |
| ------------------ | -------------------------------------------------------------------------- |
| `counts`           | How many of each entity were created.                                      |
| `appliedCount`     | Total entities successfully created.                                       |
| `skipped`          | Assets that already existed on the target (load only).                     |
| `failures`         | Entities that couldn't be created, each with a reason and optional `code`. |
| `missingVariables` | Declared `{{variables}}` you didn't supply a value for.                    |
