---
title: "Clips: Developer Guide"
description: "Quick start, the full action inventory, data model, routes, and how to extend the Clips template."
---

# Developer Guide

This page is for anyone customizing the Clips template or extending it: the quick start, every action, the data model, the route surface, and how to add to it. See [Clips](/docs/template-clips) for what the app does.

## Quick start

<Steps>

### Create the app

```bash
npx @agent-native/core@latest create my-clips --standalone --template clips
cd my-clips
pnpm install
```

This needs [Node.js](https://nodejs.org) 22.22 or later and [pnpm](https://pnpm.io) installed first. See [Getting started](/docs/getting-started) if you haven't installed either yet, or don't have an LLM connection (Builder.io, an Anthropic/OpenAI key, or local Ollama) set up.

### Start the dev server

```bash
pnpm dev
```

The app opens automatically in your browser. Recording works without any extra setup, since the browser or desktop app captures locally. You'll only hit an error on an action that needs file storage or AI credits before either is configured.

</Steps>

<Callout tone="info">

Recording, transcription, and AI titles/summaries need file storage and AI credits configured before they'll work end to end. Use Builder.io from **Settings** for both at once (its free tier includes managed AI credits), or connect S3-compatible storage separately and bring your own Groq key for transcription. See [Transcription and AI metadata](/docs/template-clips-features#transcription-and-ai-metadata).

</Callout>

## Action inventory

Every operation is a TypeScript file in `templates/clips/actions/`, auto-mounted at `POST /_agent-native/actions/:name`.

### Library & recordings

| Action                                                                                    | What it does                                             |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `list-recordings`, `search-recordings`                                                    | Library, shared-with-me, trash, and space-scoped listing |
| `create-recording`, `finalize-recording`                                                  | Create the row; finish the upload                        |
| `import-loom-recording`                                                                   | Import a Loom video or a direct MP4/WebM URL             |
| `update-recording`                                                                        | Title, description, password, expiry, visibility         |
| `move-recording`                                                                          | Move one or several recordings to a folder or root       |
| `archive-recording`, `trash-recording`, `restore-recording`, `delete-recording-permanent` | Lifecycle: hide, soft-delete, undo, or delete forever    |
| `reprocess-recording`                                                                     | Repair unseekable or frozen media                        |
| `generate-filmstrip`                                                                      | Editor timeline frame sprite                             |
| `tag-recording`, `set-thumbnail`                                                          | Tagging and a manually chosen thumbnail frame            |

### Folders & spaces

| Action                                            | What it does                                          |
| ------------------------------------------------- | ----------------------------------------------------- |
| `create-folder`, `rename-folder`, `delete-folder` | Nested folders; deleting one moves its contents up    |
| `create-space`, `rename-space`, `delete-space`    | Org-scoped, topic-based recording containers          |
| `add-recording-to-space`                          | Add or remove a recording from a space (many-to-many) |
| `add-space-member`, `remove-space-member`         | Space-level viewer, contributor, and admin roles      |

### Editing

| Action                                   | What it does                                                |
| ---------------------------------------- | ----------------------------------------------------------- |
| `trim-recording`, `split-recording`      | Layer a cut instruction; never touches the source media     |
| `remove-silences`, `remove-filler-words` | Agent-executed: reads the transcript, applies trims for you |
| `stitch-recordings`                      | Combine several recordings into one, in order               |
| `undo-edit`, `clear-edits`               | Remove the most recent edit, or reset to the original       |

### Transcription & AI metadata

| Action                                                                          | What it does                                      |
| ------------------------------------------------------------------------------- | ------------------------------------------------- |
| `request-transcript`, `cleanup-transcript`                                      | Transcribe; `force`/`regenerate` for a fresh pass |
| `regenerate-title`, `regenerate-summary`, `regenerate-chapters`, `set-chapters` | AI metadata, or set chapters manually             |
| `get-builder-credit-status`                                                     | Whether Builder.io AI credits are paused          |

### Comments & reactions

| Action                                                                                   | What it does                                 |
| ---------------------------------------------------------------------------------------- | -------------------------------------------- |
| `add-comment`, `update-comment`, `delete-comment`, `reply-to-comment`, `resolve-comment` | Threaded, timestamped comments               |
| `react-to-comment`, `react-to-recording`                                                 | Emoji reactions                              |
| `list-comments`, `list-recording-mention-members`                                        | Read comments; look up who can be @mentioned |

### Screen Memory (Rewind)

| Action                                                                                                                          | What it does                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get-screen-memory-status`, `query-screen-memory-context`                                                                       | Read the local Rewind buffer                                                                                                                              |
| `request-rewind-extension`, `get-rewind-extension-request`, `list-rewind-extension-requests`, `update-rewind-extension-request` | Deprecated compatibility actions. The editor option that used them is removed; use `request-recording-context` for earlier screen time                    |
| `apply-rewind-extension`                                                                                                        | Deprecated compatibility action. Applies an approved extension for the removed editor option                                                              |
| `make-recording-private-for-rewind`                                                                                             | Force the clip private first                                                                                                                              |
| `request-recording-context`                                                                                                     | Owner only. Ask for earlier screen time before a clip started; the clip becomes private                                                                   |
| `request-recording-context` (`deviceId`)                                                                                        | Optional. Stamps the item with the device that recorded the clip, so only that device's desktop exports it. Absent means no device on file                |
| `list-recording-context`                                                                                                        | The clip's active earlier-screen-time item: its windows and status (viewer access)                                                                        |
| `list-pending-recording-context`                                                                                                | Pending items on clips you own, for the desktop export worker                                                                                             |
| `list-pending-recording-context` (`deviceId`)                                                                                   | Optional. With `deviceId`, returns only items this device captured plus items with no device on file. Without it, the list is unfiltered (legacy clients) |
| `update-recording-context`                                                                                                      | Desktop worker reports an item as `processing`, `ready`, or `failed`                                                                                      |
| `set-recording-context-window`                                                                                                  | Trim an item inside its 5-minute original window; it returns to `pending` and re-exports                                                                  |
| `remove-recording-context`                                                                                                      | Owner only. Remove an item and trash its footage recording. The desktop edit view and the Transcript tab's panel both call it                             |
| `rewind_preview_window`, `rewind_preview_discard`                                                                               | Desktop app only (Tauri commands, not actions). Build or delete a local preview of a window. Never uploaded                                               |

### Cross-app & insights

| Action                                                                       | What it does                                                          |
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `export-to-brain`                                                            | Send ready transcripts to Brain, with backfill and a resumable cursor |
| `prepare-crm-call-evidence`                                                  | Opaque clip id plus a durable `/r/<id>` link for CRM automations      |
| `create-recording-agent-link`                                                | Mint a scoped, time-limited `agent_access` link for one recording     |
| `get-recording-insights`, `get-organization-insights`, `export-insights-csv` | Views, engagement, and org-wide CSV export                            |
| `list-viewers`, `list-clip-views`                                            | Who watched, and when                                                 |

### Organization

| Action                                                                                  | What it does                                         |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `create-organization`, `invite-member`, `accept-invite`, `decline-invite`, `get-invite` | Org creation and invitations                         |
| `remove-member`, `update-member-role`                                                   | Manage membership and roles                          |
| `set-organization-branding`, `list-organization-state`                                  | Brand color/logo, default visibility, org-wide reads |

### Navigation, embedding & bug reports

| Action                                                                                | What it does                                                                             |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `view-screen`, `navigate`, `refresh-list`                                             | Read context; move the UI                                                                |
| `get-playback-position`, `save-playback-position`, `get-recording-player-data`        | Resume playback; full transcript and diagnostics                                         |
| `create-recording-intake-link`, `create-intake-recording`, `create-intake-agent-link` | The anonymous, third-party embedding flow. See [Embed Clips](/docs/template-clips-embed) |
| `save-bug-report-context`, `save-browser-diagnostics`, `save-browser-transcript`      | The embedded bug-report launcher and Chrome extension diagnostics                        |

## Browser diagnostics

The Chrome extension attaches console and network diagnostics to a recording, and `save-browser-diagnostics` stores them. Every console and network entry carries a `tabId`.

| Topic         | Behavior                                                                                                                                                                                                                                         |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Tabs          | Up to five per recording: the start tab, then tabs active in the last 10 minutes, most recent first. A tab the user switches to during the recording is attached too.                                                                            |
| Over five     | The least recently active tab other than the focused one is detached. Its entries so far stay in the recording.                                                                                                                                  |
| Lookback      | Up to 30 seconds before the recording started, limited by the page's own console history and resource timing list. These entries have negative `elapsedMs`.                                                                                      |
| `tabId`       | Set on every console and network entry.                                                                                                                                                                                                          |
| `method`      | `"UNKNOWN"` on lookback network entries, which come from resource timing and have no HTTP method. Live requests report the method.                                                                                                               |
| App activity  | On Agent-Native apps, the [recorder](/docs/recorder) events from the recorded tab. `save-recording-app-activity` stores them as a private file with a summary on the diagnostics row, and `get-recording-app-activity` reads them by time range. |
| Not collected | Request and response bodies and headers. Console and network entries are redacted before upload.                                                                                                                                                 |

## Data model

Defined in `templates/clips/server/db/schema.ts`.

<Callout tone="warning">

Team, member, and invitation rows live in the framework's own `organizations`, `org_members`, and `org_invitations` tables, not a Clips-specific one. `organization_settings` is Clips' own sidecar: brand color, logo, and default visibility, one row per organization. An older `workspaces`/`workspace_members` pair still exists only so a one-time startup migration can backfill the framework tables on an existing deployment. Don't build new features against them.

</Callout>

<DataModel
  id="doc-block-clips-data-model"
  title="Clips data model"
  summary={
    "Recordings are the core resource. A recording belongs to one folder (or none) but can sit in any number of spaces at once. Comments and shares both key off the recording id."
  }
  entities={[
    {
      id: "recordings",
      name: "recordings",
      note: "The core resource (ownable)",
      fields: [
        { name: "id", type: "id", pk: true },
        { name: "organizationId", type: "id" },
        { name: "folderId", type: "id", nullable: true },
        {
          name: "spaceIds",
          type: "json",
          note: "JSON array of space ids, many-to-many",
        },
        { name: "title", type: "string" },
        {
          name: "status",
          type: "enum",
          note: "uploading | processing | ready | failed",
        },
        { name: "durationMs", type: "int" },
        { name: "videoUrl", type: "string", nullable: true },
        {
          name: "editsJson",
          type: "json",
          note: "layered, non-destructive edit instructions",
        },
        { name: "chaptersJson", type: "json" },
        { name: "password", type: "string", nullable: true },
        { name: "expiresAt", type: "datetime", nullable: true },
        { name: "archivedAt", type: "datetime", nullable: true },
        { name: "trashedAt", type: "datetime", nullable: true },
      ],
    },
    {
      id: "folders",
      name: "folders",
      note: "Nested folders",
      fields: [
        { name: "id", type: "id", pk: true },
        { name: "organizationId", type: "id" },
        { name: "parentId", type: "id", nullable: true },
        {
          name: "spaceId",
          type: "id",
          nullable: true,
          note: "null = personal library",
        },
        { name: "ownerEmail", type: "string" },
        { name: "name", type: "string" },
      ],
    },
    {
      id: "spaces",
      name: "spaces",
      note: "Topic-scoped org sub-containers",
      fields: [
        { name: "id", type: "id", pk: true },
        { name: "organizationId", type: "id" },
        { name: "name", type: "string" },
        { name: "isAllCompany", type: "bool" },
      ],
    },
    {
      id: "space_members",
      name: "space_members",
      note: "Per-space roles",
      fields: [
        { name: "id", type: "id", pk: true },
        { name: "spaceId", type: "id", fk: "spaces.id" },
        { name: "email", type: "string" },
        { name: "role", type: "enum", note: "viewer | contributor | admin" },
      ],
    },
    {
      id: "recording_comments",
      name: "recording_comments",
      note: "Threaded, timestamped comments",
      fields: [
        { name: "id", type: "id", pk: true },
        { name: "recordingId", type: "id", fk: "recordings.id" },
        { name: "threadId", type: "id" },
        { name: "parentId", type: "id", nullable: true },
        { name: "authorEmail", type: "string" },
        { name: "content", type: "string" },
        { name: "videoTimestampMs", type: "int" },
        { name: "resolved", type: "bool" },
      ],
    },
    {
      id: "recording_shares",
      name: "recording_shares",
      note: "Share grants, from the shared framework sharing primitive",
      fields: [
        { name: "id", type: "id", pk: true },
        { name: "recordingId", type: "id", fk: "recordings.id" },
        { name: "principal", type: "string", note: "user or org" },
        { name: "role", type: "enum", note: "viewer | editor | admin" },
      ],
    },
  ]}
  relations={[
    { from: "folders", to: "recordings", kind: "1-n", label: "contains" },
    {
      from: "spaces",
      to: "recordings",
      kind: "n-n",
      label: "recordings can join any number of spaces",
    },
    { from: "spaces", to: "space_members", kind: "1-n", label: "has members" },
    {
      from: "recordings",
      to: "recording_comments",
      kind: "1-n",
      label: "has comments",
    },
    {
      from: "recordings",
      to: "recording_shares",
      kind: "1-n",
      label: "has share grants",
    },
  ]}
/>

## An action walked through

`create-recording-agent-link` shows patterns worth reusing in any action that mints a scoped credential: checking the resource's real state before handing anything out, delegating the actual token minting to a shared framework helper instead of rolling your own, and deriving every URL a caller needs from that one grant.

<AnnotatedCode
  id="doc-block-clips-agent-link"
  title="Minting a scoped agent-access link"
  filename="templates/clips/actions/create-recording-agent-link.ts"
  language="ts"
  code={
    'import { defineAction } from "@agent-native/core/action";\nimport {\n  buildAgentAccessUrl,\n  createScopedAgentAccessGrant,\n  getRequestContext,\n  getRequestUserEmail,\n} from "@agent-native/core/server";\nimport { ForbiddenError, resolveAccess } from "@agent-native/core/sharing";\nimport { z } from "zod";\n\nimport {\n  CLIPS_AGENT_ACCESS_TTL_SECONDS,\n  getServerAppBasePath,\n} from "../server/lib/public-agent-context.js";\nimport {\n  buildAgentApiUrls,\n  CLIP_AGENT_ACCESS_TOKEN_PREFIX,\n} from "../shared/agent-context.js";\nimport { BUG_REPORT_AGENT_ACCESS_TTL_SECONDS } from "../shared/bug-report.js";\n\nfunction appOrigin(): string {\n  const origin =\n    getRequestContext()?.requestOrigin ||\n    process.env.APP_URL ||\n    process.env.BETTER_AUTH_URL ||\n    "http://localhost:3000";\n  try {\n    return new URL(origin).origin;\n  } catch {\n    return "http://localhost:3000";\n  }\n}\n\nexport default defineAction({\n  description:\n    "Create a temporary private agent-readable link for one Clips recording. The URL is scoped to that recording and expires after two hours.",\n  schema: z.object({\n    recordingId: z.string().describe("Recording ID"),\n    agentLabel: z\n      .string()\n      .trim()\n      .min(1)\n      .max(60)\n      .optional()\n      .describe(\n        "Name of the agent that will read this link (e.g. \'Fusion\', \'Claude Code\'). Recorded against every read the link produces, so the owner sees a name instead of an unidentified agent.",\n      ),\n    ttlSeconds: z\n      .number()\n      .int()\n      .positive()\n      .max(BUG_REPORT_AGENT_ACCESS_TTL_SECONDS)\n      .optional()\n      .describe(\n        "Optional scoped-link lifetime in seconds, capped at seven days. Defaults to two hours.",\n      ),\n  }),\n  readOnly: true,\n  run: async (args) => {\n    const access = await resolveAccess("recording", args.recordingId);\n    if (!access) {\n      throw new ForbiddenError(`No access to recording ${args.recordingId}`);\n    }\n\n    const recording = access.resource as {\n      id: string;\n      archivedAt?: string | null;\n      trashedAt?: string | null;\n    };\n    if (recording.archivedAt || recording.trashedAt) {\n      throw new ForbiddenError(\n        `Recording ${args.recordingId} is not shareable`,\n      );\n    }\n\n    const grant = createScopedAgentAccessGrant({\n      resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,\n      resourceId: recording.id,\n      viewerEmail: getRequestUserEmail() || undefined,\n      agentLabel: args.agentLabel,\n      ttlSeconds: args.ttlSeconds ?? CLIPS_AGENT_ACCESS_TTL_SECONDS,\n    });\n    const origin = appOrigin();\n    const basePath = getServerAppBasePath();\n    const pageUrl = buildAgentAccessUrl({\n      path: `/share/${encodeURIComponent(recording.id)}`,\n      origin,\n      basePath,\n      token: grant.token,\n    });\n    const api = buildAgentApiUrls(recording.id, {\n      origin,\n      basePath,\n      token: grant.token,\n    });\n\n    return {\n      recordingId: recording.id,\n      url: pageUrl,\n      contextUrl: api.contextUrl,\n      expiresAt: grant.expiresAt,\n      ttlSeconds: grant.ttlSeconds,\n    };\n  },\n});\n'
  }
  annotations={[
    {
      lines: "60-74",
      label: "Resource state is checked before anything is minted",
      note: "resolveAccess() confirms the caller can see this recording at all, then an explicit archivedAt/trashedAt check refuses to hand out a link for a clip that shouldn't be shareable anymore, even to someone who technically still has access to it.",
    },
    {
      lines: "76-82",
      label: "Token minting is delegated, not reinvented",
      note: "createScopedAgentAccessGrant() is a shared framework helper, so Clips doesn't own any token-signing logic. The TTL is capped by the zod schema and defaults to a named constant rather than a magic number.",
    },
    {
      lines: "83-95",
      label: "Every URL a caller needs comes from the same grant",
      note: "The share page URL and the three agent API URLs (buildAgentApiUrls) are all derived from one token, so there's exactly one place that could leak or reuse a grant incorrectly, not three.",
    },
    {
      lines: "79",
      label: "The link remembers who asked for it",
      note: "viewerEmail and agentLabel are recorded against the grant, so a recording's owner sees which person and which agent read it, instead of an anonymous access log entry.",
    },
  ]}
/>

## Routes reference

<FileTree
  id="doc-block-clips-routes"
  title="Clips template layout"
  entries={[
    {
      path: "actions/",
      note: "every agent-callable action — recordings, folders, spaces, comments, sharing, insights",
    },
    {
      path: "app/routes/_app.library._index.tsx",
      note: "the Library grid; _app.library.folder.$folderId.tsx is one folder",
    },
    { path: "app/routes/_app.shared.tsx", note: "Shared with me" },
    {
      path: "app/routes/_app.spaces._index.tsx",
      note: "Spaces list; _app.spaces.$spaceId.tsx is one space",
    },
    {
      path: "app/routes/_app.r.$recordingId.tsx",
      note: "the recording detail page and editor",
    },
    { path: "app/routes/_app.agent.tsx", note: "full-page agent chat tab" },
    {
      path: "app/routes/_app.settings._index.tsx",
      note: "Settings, including the Labs tab",
    },
    { path: "app/routes/record.tsx", note: "the recorder" },
    {
      path: "app/routes/share.$shareId.tsx",
      note: "the public share page; embed.$shareId.tsx is the iframe-embed variant",
    },
    {
      path: "app/routes/download.tsx",
      note: "the desktop app download page",
    },
    {
      path: "server/db/schema.ts",
      note: "Drizzle tables — recordings, folders, spaces, comments",
    },
    {
      path: "server/routes/api/agent-context.json.get.ts",
      note: "the agent-readable endpoints, plus agent-transcript.json, agent-frame.jpg and agent-frames.jpg",
    },
    {
      path: "server/lib/google-calendar-client.ts",
      note: "the Google Calendar API client",
    },
    {
      path: "shared/api.ts",
      note: "shared TypeScript types used by server and client",
    },
    {
      path: "desktop/",
      note: "the Tauri desktop app (screen/system-audio capture, Rewind)",
    },
    {
      path: "chrome-extension/",
      note: "the Chrome extension (recording plus browser diagnostics)",
    },
  ]}
/>

## Customizing it

Every part of the app is editable source. Start here:

- `templates/clips/actions/` — add a new file with `defineAction` to expose new capability to both the agent and the frontend.
- `templates/clips/app/routes/` — the UI, listed above.
- `templates/clips/server/db/schema.ts` — add columns or tables with Drizzle. Keep the schema PostgreSQL-compatible so the template runs with local PGlite and hosted Postgres.
- `templates/clips/server/lib/` — provider integrations: `google-calendar-client.ts`, `s3-upload-provider.ts`, `slack-oauth.ts`, `video-remux.ts`, `video-filmstrip-sprite.ts`.
- `templates/clips/AGENTS.md` — agent instructions. Update this when you teach the agent new capabilities or conventions.
- `templates/clips/.agents/skills/` — detailed patterns the agent follows. Relevant skills: `recording`, `ai-video-tools`, `video-editing`, `video-sharing`, `screen-memory`.
- `templates/clips/shared/api.ts` — the shared TypeScript types used by both the server and the client.
- `templates/clips/desktop/` and `templates/clips/chrome-extension/` — separate packages for the native capture surfaces, each with their own `package.json` and build.

If you add a feature, remember to update all four areas: UI, action, skill or AGENTS.md entry, and any application state the agent needs to see. That is what keeps the agent and the UI in parity.

## What's next

- [**Clips**](/docs/template-clips) — the overview and what it replaces
- [**Features**](/docs/template-clips-features) — the user-facing behavior these actions and this data model power
- [**Talk to the Agent**](/docs/template-clips-agent) — how the agent chooses between these actions and less common ones
- [**Clips in a Multi-App Workspace**](/docs/template-clips-integrations) — how the agent-readable endpoints and `clip.created` event get used by other apps
- [**Embed Clips**](/docs/template-clips-embed) — the anonymous, third-party recording-intake flow
- [**Actions**](/docs/actions-overview) — the action system this template is built on
- [**Sharing**](/docs/sharing) — the share-grant model `recording_shares` uses
