Loopfour

Dashboard Templates

How dashboard tiles map to canonical workflow templates, and how to add a new one.

Dashboard Templates

The tiles on the chat dashboard (/chat) and the gallery (/workflows?tab=templates) are both backed by the same system templates — rows in workflow_templates with company_id IS NULL and a stable slug (e.g. cash-application).

How the pieces fit

packages/db/src/templates/dashboard/<slug>.ts   ← canonical workflow JSON
        │ (compile-time registry: DASHBOARD_TEMPLATE_DEFINITIONS)

pnpm db:seed:dashboard-templates                 ← upserts into workflow_templates


GET /api/v1/templates/by-slug/:slug              ← dashboard / chat tools resolve here
GET /api/v1/templates/gallery                    ← gallery list

Slugs are unique among system templates (UNIQUE (slug) WHERE company_id IS NULL). They never change across environments — a UUID is generated by Postgres on insert and may differ between dev/staging/prod, but the slug is authored in code and is identical everywhere the seeder runs.

Adding a new dashboard template

Three small changes:

1. Card metadata

Add the tile entry in apps/studio/lib/dashboard/templates.ts:

{
  id: "ap-payment-batching",         // kebab-case, must be unique
  title: "AP payment batching",
  description: "...",
  category: "ap",                     // ar | ap | billing | close | revrec
  metaItems: ["Bill.com", "Daily"],
  integrations: integrations("billcom"),
  previewSteps: ["Pull approved bills", "Group by vendor", "Queue ACH"],
  prompt: "Build a workflow that ...",
  live: true,
}

2. Canonical JSON

Drop a new file in packages/db/src/templates/dashboard/<slug>.ts:

import { defineDashboardTemplate } from "./_shared.js";

export const apPaymentBatchingTemplate = defineDashboardTemplate({
  templateId: "ap-payment-batching",
  name: "AP payment batching",
  description: "...",
  category: "finance",
  tags: ["dashboard-card", "ap"],
  requiredConnections: ["billcom"],
  variables: [],
  trigger: { type: "schedule", cron: "0 8 * * *", timezone: "UTC", enabled: true },
  steps: [
    { id: "pull_bills", type: "action", name: "Pull approved bills", action: "billcom.listApprovedBills", config: {} },
    // …
  ],
});

defineDashboardTemplate is an identity helper that pins the literal templateId type and throws at module-load time if a duplicate slug is registered.

3. Registry index

Import and register in packages/db/src/templates/dashboard/index.ts:

import { apPaymentBatchingTemplate } from "./ap-payment-batching.js";

export const DASHBOARD_TEMPLATE_DEFINITIONS = {
  // …
  "ap-payment-batching": apPaymentBatchingTemplate,
} as const satisfies Record<string, DashboardTemplateDefinition>;

The Record<DashboardTemplateId, …> constraint in Studio fails the build if a card id has no matching template, so missing this step is caught at compile time.

Deploying

After committing template changes:

pnpm --filter @workflows/db build
pnpm --filter @workflows/db db:seed:dashboard-templates

The seeder is idempotent — safe to re-run on every deploy. It upserts on (slug) WHERE company_id IS NULL, preserving each row's UUID and any stars, just patching mutable fields (name, description, steps, etc.).

Don't

  • Don't rename a slug after it ships. Slugs are stable identifiers; renaming one breaks every reference (chat tools, deep links, audit logs).
  • Don't set slug from POST /api/v1/templates. Slugs are seeder-only; company-specific templates created via the API always have slug = NULL.
  • Don't use the category enum to express dashboard subcategories. Category stays finance (the API enum); dashboard subcategories live in tags (ar, ap, billing, close, revrec).

On this page