DEV Community

Cover image for Add a "New from Template" Gallery to Your SaaS in Two API Calls
ByteChef
ByteChef

Posted on Originally published at blog.bytechef.io

Add a "New from Template" Gallery to Your SaaS in Two API Calls

TL;DR — A "New from Template" gallery in your SaaS takes two REST calls with ByteChef Embedded: GET /automation/projects returns your curated catalog, and POST /automation/workflow-templates/{uuid}/copy clones 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:

Sample app create menu: New from Template, New from Embedded Workflow Builder, New from Prompt, New from Chat, New from Custom Workflow Builder

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.

Choose a workflow template page with Automation Playground and Team productivity sections

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:

ByteChef Automation Workflows page with two published projects

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:

Create Project dialog for Team productivity

  • 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:

Create Workflow dialog for Daily standup reminder

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):

Workflow code editor with a schedule trigger and a Slack sendChannelMessage task

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:

Editor with Missing required property: channel and Missing required connection: Slack

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:

Project menu with Publish, New Workflow, Import Workflow, Edit and Delete

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();
}
Enter fullscreen mode Exit fullscreen mode

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": []
      }
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode
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"
/>
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
    });
};
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode

That one POST does a lot:

  1. Copies the latest published version from the user's permission-filtered catalog. Drafts never leak.
  2. Keeps the label and description — no "(Copy)" suffix. To the user, it's just their automation.
  3. Lands in the user's own project, auto-created per user and environment. Your app never manages projects.
  4. 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}
/>
Enter fullscreen mode Exit fullscreen mode

Here's a copy of the Greeting Reference template, now in the user's workspace:

Embedded editor showing the copied Greeting Reference workflow marked V1 DRAFT

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:

Workflows list with the copied workflow at the top marked DRAFT and a provisioned reference at the bottom

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'});
Enter fullscreen mode Exit fullscreen mode

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 — hide REFERENCE templates 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 — components only 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

  1. Connect your users' apps (JWT + ConnectDialog)
  2. Act on connected apps with the ComponentKit
  3. Chat with an AI assistant using ComponentKit tools
  4. Trigger workflows from your product's events
  5. Call a workflow like a synchronous API
  6. Agents over MCP
  7. Author workflows in your own custom builder
  8. 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)