TL;DR — A "New from Template" gallery in your SaaS takes two REST calls with ByteChef Embedded:
GET /automation/projectsreturns your curated catalog, andPOST /automation/workflow-templates/{uuid}/copyclones a template into the user's own workspace. The copy is a normal, editable workflow that starts as a draft. This is part 8 of the ByteChef Embedded series.
Most users don't want a blank canvas. In part 7 we let users build workflows from scratch in a UI we designed — flexible, but slow. What users actually want to hear is: "Here's an automation we already built for you. Want it?"
That's what templates are. You design a few great workflows once, in ByteChef, and every customer gets them as a one-click starting point inside your product.
Why templates get the big button
In the embedded sample app, New from Template is the primary action on the Automations page. Every other option lives in the split menu:
The reason is simple: every other option asks the user to describe an automation (on a canvas, in a prompt, in a form). A template only asks them to pick one. For onboarding, that's the shortest path from "I just signed up" to "something is running for me."
What the user sees
Clicking the button opens /automations/templates: one section per project, one card per template.
Everything on a card comes from ByteChef:
- the template's label (Daily standup reminder),
- the description you wrote,
- a row of component icons for the apps it uses (here: Slack).
Click a card → the template is copied → the user lands in their new workflow. The rest of this post is what happens on each side of that click.
Step 1: Build a template in ByteChef
Templates live in ByteChef, not in your app. Let's build Daily standup reminder: every weekday at 9:00, post a reminder to Slack.
Open Automation Workflows
In ByteChef go to Embedded → Automations. You'll see your catalog projects, each with a version badge:
This page only exists in the Development environment, so you author templates in one place. The published catalog, however, is served to connected users in every environment.
Create a project
A project is a shelf in your gallery. Click New Project:
- Name and Description become the section heading and subheading — write them for customers.
- Category and Tags only organize this admin page; they're not in the API response.
- Permission Expression limits who sees the whole project (more below).
- Show in Automation Hub controls ByteChef's own embeddable Hub. Turn it off for flows you only expose through the API.
Save, and the project shows up with a DRAFT badge.
Add a workflow
Each workflow = one template card. Click + Workflow on the project row:
Label and Description become the card's title and text. Workflows have their own Permission Expression too, so you can hide one template inside an otherwise public project.
Now build it — on the canvas, or by pasting a definition into the Workflow Code Editor (the </> button):
It's a schedule/v1/everyDay trigger (9:00, dayOfWeek 2–6) plus one Slack Send Channel Message task. Close the editor and ByteChef flags two issues:
Leave them. You can't know which Slack workspace or channel each customer uses. A good template fills in everything that's the same for every customer (schedule, message, structure) and leaves the account-specific bits blank.
Publish
Templates are invisible until the project is published. Open the project's ⋮ menu and click Publish:
No confirmation, no validation gate — the badge just flips from DRAFT to V1 PUBLISHED. A few things worth knowing:
- Users always get the latest published version, never your work in progress. Edit freely; nothing changes until you publish V2.
- An unpublished project still appears in the API, but empty — so publish before you tell customers to look.
- Copies don't follow new versions. V2 only affects new copies. (References behave differently — see below.)
Step 2: Fetch the catalog
The gallery loads everything with one call:
export async function fetchAutomationWorkflowProjects(): Promise<AutomationWorkflowProject[]> {
const response = await fetchWithAuth('/api/embedded/v1/automation/projects', {
method: 'GET',
});
if (!response.ok) {
throw new Error(`Failed to fetch automation workflow projects: ${response.status}`);
}
return response.json();
}
fetchWithAuth sends the connected user's JWT (from part 1) plus an X-Environment header (defaults to PRODUCTION). The JWT matters here because the catalog is filtered per user.
The response looks like this (icon shortened):
[
{
"id": 1128,
"name": "Team productivity",
"description": "Ready-made workflows that keep your team in sync",
"automationHubVisible": true,
"kind": "COPY",
"workflowTemplates": [
{
"id": "6a62a705-2225-445d-a91a-048607332571",
"label": "Daily standup reminder",
"description": "Every weekday at 9:00, remind your team in Slack that standup is about to start",
"components": [
{ "name": "slack", "title": "Slack", "icon": "<svg ...>...</svg>" }
],
"inputs": []
}
]
}
]
| Field | Meaning |
|---|---|
name, description
|
Section heading + subheading |
automationHubVisible |
The Hub switch. Not used for filtering — a hint for your UI |
kind |
COPY (normal projects) or REFERENCE (code-native projects) |
workflowTemplates[].id |
The UUID you pass to the copy call |
workflowTemplates[].components |
Apps used: name, title, SVG icon
|
workflowTemplates[].inputs |
Declared workflow inputs |
Two gotchas:
components lists tasks, not the trigger. A "New HubSpot deal → Slack" template would only show Slack. If the trigger app matters, mention it in the label or description.
icon is raw SVG, not a URL. No hosting or proxying needed:
<img
src={`data:image/svg+xml;utf8,${encodeURIComponent(component.icon)}`}
alt={component.title || component.name}
className="size-full"
/>
Who sees which template
Projects and workflows can each carry a Permission Expression — a SpEL condition evaluated against the connected user:
metadata['plan'] == 'pro'
metadata['tier'] == 'enterprise'
Expressions can read the user's metadata, externalId, email, name and environment. That gives you tiered catalogs with zero app code.
The rules are fail-closed: empty means everyone; otherwise it must evaluate to true. A false or a broken expression hides the item, so a typo hides a template instead of leaking it. The same check runs on copy, so guessing a UUID doesn't help — ByteChef answers as if the template doesn't exist.
Step 3: Render the gallery
This part is plain React. The sample's app/automations/templates/page.tsx keeps four bits of state:
const [projects, setProjects] = useState<AutomationWorkflowProject[] | undefined>();
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [creatingWorkflowId, setCreatingWorkflowId] = useState<string | null>(null);
One <section> per project, one shadcn/ui Card per template, in a 1–3 column grid. While a copy is in flight, the clicked card reads Creating workflow... and every card gets aria-disabled, so a double click can't start two copies.
Nothing here is ByteChef-specific. Cards, a searchable list, an onboarding checklist — the API gives you data, not UI.
Step 4: One click, one copy
const handleSelectWorkflowTemplate = (workflowTemplateUuid: string) => {
setCreatingWorkflowId(workflowTemplateUuid);
setError(null);
copyWorkflowTemplate(workflowTemplateUuid)
.then((workflowUuid) => router.push(`/automations/${workflowUuid}`))
.catch(() => {
setError('Failed to create a workflow from the selected template');
setCreatingWorkflowId(null);
});
};
export async function copyWorkflowTemplate(workflowUuid: string): Promise<string> {
const response = await fetchWithAuth(
`/api/embedded/v1/automation/workflow-templates/${workflowUuid}/copy`,
{method: 'POST'}
);
if (!response.ok) {
throw new Error(`Failed to copy workflow template: ${response.status}`);
}
const newWorkflowUuid = (await response.text()).trim();
// The endpoint may return the uuid as a JSON string or as plain text.
return newWorkflowUuid.startsWith('"') && newWorkflowUuid.endsWith('"')
? newWorkflowUuid.slice(1, -1)
: newWorkflowUuid;
}
That one POST does a lot:
- Copies the latest published version from the user's permission-filtered catalog. Drafts never leak.
- Keeps the label and description — no "(Copy)" suffix. To the user, it's just their automation.
- Lands in the user's own project, auto-created per user and environment. Your app never manages projects.
- Remembers its origin and allows one copy per template per user. A second click fails — in production, show an Added badge that opens the existing copy instead.
What the user gets
The sample routes to /automations/{uuid}, which renders ByteChef's embedded editor:
<EmbeddedWorkflowBuilder
baseUrl={BYTECHEF_APP_BASE_URL}
connectionDialogAllowed={true}
environment={BYTECHEF_ENVIRONMENT}
jwtToken={jwtToken}
workflowUuid={workflowUuid}
/>
Here's a copy of the Greeting Reference template, now in the user's workspace:
Note the V1 DRAFT badge. A copy is never live on arrival — a template might post to Slack or send email, and nothing should run on a customer's accounts until they've reviewed it and picked which accounts to use. (A copy of Daily standup reminder arrives with the two open issues we left in, so the user's first job is choosing their Slack connection and channel.)
Back on the Automations list, the new copy sits at the top as DRAFT:
The row at the bottom came from the same template — but as a reference, not a copy. More on that in a second.
From draft to running
Same three calls as part 7, authenticated with the user's JWT:
// 1. Bind the user's connection to each node that needs one
await fetchWithAuth(
`/api/embedded/v1/automation/workflows/${workflowUuid}/workflow-nodes/slack_1/connection/slack`,
{
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({connectionId}),
}
);
// 2. Freeze the current definition as V1
await fetchWithAuth(`/api/embedded/v1/automation/workflows/${workflowUuid}/publish`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({description: 'Created from template'}),
});
// 3. Turn it on (DELETE to turn it off)
await fetchWithAuth(`/api/embedded/v1/automation/workflows/${workflowUuid}/enable`, {method: 'POST'});
The sample leaves these to the user (Create Connection in the editor, Publish, then the enable toggle). But you can also wrap them in a short "Activate" wizard: pick a Slack account from GET /components/slack/connections, fill any inputs, then bind → publish → enable. That's exactly what ByteChef's own Automation Hub does.
Copy or reference?
There are two ways to hand a user a catalog workflow. The difference is who owns it afterwards.
| Copy | Reference | |
|---|---|---|
| Call | POST .../{uuid}/copy |
POST .../{uuid}/provision |
| User gets | A new, independent workflow | A per-user deployment of your workflow |
| Editable? | Yes, it's theirs | No, it runs your definition |
| You publish V2 | Nothing changes (snapshot) | Every reference rolls forward |
| Connections | Bound per node after copy | Auto-wired or passed in the request |
| Initial state | Draft | Enabled once connections/inputs resolve |
Copy when the template is a starting point users will customize. Reference when the workflow is really part of your product and you want to ship fixes once to everyone.
Check kind: COPY projects support both; REFERENCE (code-native) projects can only be provisioned — copying returns 409 with {"reason": "CODE_WORKFLOW_NOT_COPYABLE"}.
Server-to-server variants
Every call has a backend form authenticated with your environment's API secret key:
| Browser (JWT) | Backend (API key) |
|---|---|
GET /automation/projects |
GET /{externalUserId}/automation/projects |
POST /automation/workflow-templates/{uuid}/copy |
POST /{externalUserId}/automation/workflow-templates/{uuid}/copy |
Use these when the decision isn't the user's — e.g. drop a "Welcome" template into every new account at signup. Permission expressions still apply to the user in the path.
Production checklist
- [ ] Respect
kind— hideREFERENCEtemplates or route them to/provision. - [ ] Track what the user already has — show Added / Open instead of letting a second copy fail.
- [ ] Surface the trigger's app —
componentsonly lists tasks. - [ ] Decide on
automationHubVisible— the API returns hidden projects too. - [ ] Distinguish errors — "template no longer available" (404) vs. transient failures.
- [ ] Offer activation, not just a copy — a two-step wizard gets users to a running automation in under a minute.
Who owns what
- You own curation: which workflows exist, how they're grouped, who sees them, when they change, and how the gallery looks.
- ByteChef owns the machinery: the editor, versioning, per-user filtering, tenant-isolated copies, component metadata and icons, connection storage, and the engine that runs it all.
Your code for the entire feature: two API calls and a grid of cards.
The series so far
- Connect your users' apps (JWT + ConnectDialog)
- Act on connected apps with the ComponentKit
- Chat with an AI assistant using ComponentKit tools
- Trigger workflows from your product's events
- Call a workflow like a synchronous API
- Agents over MCP
- Author workflows in your own custom builder
- Start from a curated template (this post)
Parts 7 and 8 are two ends of the same spectrum: a custom builder gives full control and asks a lot; a template asks almost nothing and gets users to value fastest. Most products want both — and both produce the same thing: a real ByteChef workflow in the user's own project.
Try it: in ByteChef, open Embedded → Automations, create a project with a workflow or two, and Publish. Then run the embedded sample app, open Automations → New from Template, and click a card.
Originally published on the ByteChef blog.










Top comments (0)