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 listSlugs 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-templatesThe 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
slugfromPOST /api/v1/templates. Slugs are seeder-only; company-specific templates created via the API always haveslug = NULL. - Don't use the
categoryenum to express dashboard subcategories. Category staysfinance(the API enum); dashboard subcategories live intags(ar,ap,billing,close,revrec).
Loopfour