<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ByteChef</title>
    <description>The latest articles on DEV Community by ByteChef (@bytechef).</description>
    <link>https://dev.to/bytechef</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3622449%2F5f75e94d-42ad-41ff-9035-22082f2f8fc9.png</url>
      <title>DEV Community: ByteChef</title>
      <link>https://dev.to/bytechef</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bytechef"/>
    <language>en</language>
    <item>
      <title>Run a ByteChef workflow like a CLI command (no server, no database)</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:15:17 +0000</pubDate>
      <link>https://dev.to/bytechef/run-a-bytechef-workflow-like-a-cli-command-no-server-no-database-1n4d</link>
      <guid>https://dev.to/bytechef/run-a-bytechef-workflow-like-a-cli-command-no-server-no-database-1n4d</guid>
      <description>&lt;p&gt;Most automation platforms are built to stay up. ByteChef is no exception: the server stores your projects, fires triggers, listens for webhooks, and records every execution in PostgreSQL.&lt;/p&gt;

&lt;p&gt;That's great until you look at where a lot of real work runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a nightly batch sitting in a Kubernetes CronJob next to twenty others, with alerting on failed pods&lt;/li&gt;
&lt;li&gt;a CI step that should go red when something breaks&lt;/li&gt;
&lt;li&gt;one task inside an Airflow DAG&lt;/li&gt;
&lt;li&gt;a product with its own job runner that just wants to execute a workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In each of those, a standing server plus a database plus a message broker is overhead. You need the engine for the length of one run, and then you want it gone.&lt;/p&gt;

&lt;p&gt;That's what the &lt;strong&gt;Runtime Job app&lt;/strong&gt; does. You pass it a workflow name, inputs as JSON, and credentials as JSON. It boots the full Atlas engine in one JVM, runs the workflow to completion, and exits &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;1&lt;/code&gt;. Nothing to provision, nothing to clean up.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Runtime Job app is part of ByteChef's Enterprise Edition. Source: &lt;code&gt;server/ee/apps/runtime-job-app&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Same engine, different packaging
&lt;/h2&gt;

&lt;p&gt;This isn't a lightweight interpreter. The coordinator, workers and task dispatchers are all there. What changes is everything around them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ByteChef server&lt;/th&gt;
&lt;th&gt;Runtime Job app&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Execution state&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;In memory, gone on exit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message broker&lt;/td&gt;
&lt;td&gt;Redis, Kafka, SQS, RabbitMQ, JMS…&lt;/td&gt;
&lt;td&gt;In-process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connections&lt;/td&gt;
&lt;td&gt;Encrypted store in the DB&lt;/td&gt;
&lt;td&gt;JSON passed at launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triggers&lt;/td&gt;
&lt;td&gt;Scheduler, polling, webhooks&lt;/td&gt;
&lt;td&gt;None. The run starts with the process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI / API&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Record of a run&lt;/td&gt;
&lt;td&gt;Execution history&lt;/td&gt;
&lt;td&gt;Exit code + log&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The workflow JSON is identical in both, so the loop is: build and test in ByteChef, export, hand the file to the Runtime Job app.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example
&lt;/h2&gt;

&lt;p&gt;A daily sales report: take a date, total the paid orders for that day from PostgreSQL, log a summary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Daily Sales Report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reportDate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Report date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"triggers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Manual"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"trigger_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manual/v1/manual"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sum paid orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgresql_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgresql/v1/query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECT count(*) AS orders, coalesce(sum(total), 0) AS revenue FROM orders WHERE status = 'PAID' AND created_at = CAST(:reportDate AS date)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reportDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${reportDate}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Log summary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logger_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logger/v1/info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sales for ${reportDate}: ${postgresql_1[0].orders} paid orders, revenue ${postgresql_1[0].revenue}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;~/bytechef/data/workflows/daily-sales.json&lt;/code&gt; (the default lookup location). Note the task name &lt;code&gt;postgresql_1&lt;/code&gt; and component name &lt;code&gt;postgresql&lt;/code&gt;; both matter for connections later.&lt;/p&gt;

&lt;p&gt;Build and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew :server:ee:apps:runtime-job-app:bootJar

&lt;span class="nv"&gt;CONNECTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"postgresql": {"host": "localhost", "port": 5432, "database": "shop", "username": "etl", "password": "..."}}'&lt;/span&gt;

java &lt;span class="nt"&gt;-jar&lt;/span&gt; runtime-job-app.jar &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;daily-sales.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"reportDate": "2026-10-14"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connections&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONNECTIONS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;   &lt;span class="c"&gt;# 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a laptop the whole thing took about 10 seconds, roughly one of which was the workflow itself. That's the trade: a few seconds of boot for zero standing infrastructure. Fine for a nightly batch; for a thousand calls a minute, use the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flags
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--workflow=&amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;File name, e.g. &lt;code&gt;daily-sales.json&lt;/code&gt;. A lookup key, &lt;strong&gt;not&lt;/strong&gt; a path.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--parameters=&amp;lt;json&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Workflow inputs as a JSON object.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--connections=&amp;lt;json&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Connection name → parameters. Repeatable; objects are merged.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--timeout=&amp;lt;duration&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;30s&lt;/code&gt;, &lt;code&gt;15m&lt;/code&gt;, &lt;code&gt;1h&lt;/code&gt;. Exit &lt;code&gt;1&lt;/code&gt; if exceeded.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--outputs-file=&amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Write the workflow's outputs as JSON on success.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Any Spring Boot property works too, e.g. &lt;code&gt;--logging.level.com.bytechef=DEBUG&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha: &lt;code&gt;--workflow&lt;/code&gt; is not a path
&lt;/h3&gt;

&lt;p&gt;The app drops the directory, strips the extension, and Base64-encodes the rest into an id (&lt;code&gt;daily-sales&lt;/code&gt; → &lt;code&gt;ZGFpbHktc2FsZXM=&lt;/code&gt;). At startup it indexes every file in the enabled workflow repositories the same way, then looks the id up. So these are identical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--workflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;daily-sales.json
&lt;span class="nt"&gt;--workflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/anything/at/all/daily-sales.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and pointing at a file outside the repositories won't work. Repositories available: filesystem (default, &lt;code&gt;~/bytechef/data/workflows&lt;/code&gt;), classpath (&lt;code&gt;workflows/&lt;/code&gt; inside the JAR), and Git (off by default).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# read workflows from somewhere else&lt;/span&gt;
&lt;span class="nv"&gt;BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/opt/workflows/*.json'&lt;/span&gt;

&lt;span class="c"&gt;# or straight from Git, so a merge to main is a deploy&lt;/span&gt;
&lt;span class="nv"&gt;BYTECHEF_WORKFLOW_REPOSITORY_GIT_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;BYTECHEF_WORKFLOW_REPOSITORY_GIT_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://github.com/acme/workflows.git
&lt;span class="nv"&gt;BYTECHEF_WORKFLOW_REPOSITORY_GIT_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; export workflows from the editor rather than hand-writing them. Component defaults are written into the JSON when you add a node, not at runtime, so a hand-written file can fail mid-run with &lt;code&gt;Unknown value for : &amp;lt;property&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connections
&lt;/h2&gt;

&lt;p&gt;No credential store means credentials arrive as JSON. Keys are connection names; values use the same property names as the component's connection form in the UI.&lt;/p&gt;

&lt;p&gt;Before each task runs, its connection is resolved like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;By task name&lt;/strong&gt; (&lt;code&gt;postgresql_1&lt;/code&gt;) — applies to that task only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By component name&lt;/strong&gt; (&lt;code&gt;postgresql&lt;/code&gt;) — shared by every task of that component.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No match&lt;/strong&gt; — the task runs without one (fine for &lt;code&gt;logger&lt;/code&gt;, fatal for a database).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Task-name keys let two tasks of the same component use different credentials, say a read replica and a primary. And you can split credentials across secrets by repeating the flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; runtime-job-app.jar &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;daily-sales.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connections&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OPENAI_CONNECTION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connections&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_CONNECTION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two caveats worth knowing up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection JSON is logged at INFO on startup.&lt;/strong&gt; Treat the log as credential-bearing: restrict access and set retention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every connector works this way yet.&lt;/strong&gt; Database components (PostgreSQL, MySQL) and LLM providers that take an API key work. SaaS connectors whose auth is applied as an HTTP header by the platform (bearer, API key, OAuth2) don't get that header from &lt;code&gt;--connections&lt;/code&gt;. Test before relying on them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exit codes are the API
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job &lt;code&gt;COMPLETED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job &lt;code&gt;FAILED&lt;/code&gt; or &lt;code&gt;STOPPED&lt;/code&gt;, or &lt;code&gt;--timeout&lt;/code&gt; hit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other non-zero&lt;/td&gt;
&lt;td&gt;Couldn't start: missing flag, bad JSON, unknown workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;143&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Killed by SIGTERM (eviction, drain, &lt;code&gt;docker stop&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's all a scheduler needs. A Kubernetes Job retries per &lt;code&gt;backoffLimit&lt;/code&gt;, Airflow marks the task failed, CI goes red. Nobody parses logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always set &lt;code&gt;--timeout&lt;/code&gt;.&lt;/strong&gt; Without it the app waits forever, and a stuck process is never retried because it never fails. This matters doubly for JavaScript/Python script tasks, which have no CPU or heap cap of their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make writes idempotent.&lt;/strong&gt; A &lt;code&gt;143&lt;/code&gt; leaves no trace in the log, and a retry starts from scratch. Prefer upserts or check for an existing result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting results out
&lt;/h2&gt;

&lt;p&gt;Declare outputs in the workflow as usual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${postgresql_1[0].orders}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"revenue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${postgresql_1[0].revenue}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They're logged as one JSON line on completion. For programs, use &lt;code&gt;--outputs-file&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; runtime-job-app.jar ... &lt;span class="nt"&gt;--outputs-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/daily-sales.json
jq .revenue /tmp/daily-sales.json   &lt;span class="c"&gt;# 1540.50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file is only written for a completed job, so check the exit code first. Keep outputs small (counts, ids, a status); write anything big to where it's used and return a pointer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; registry.example.com/bytechef-runtime-job:2026-10-16 server/ee/apps/runtime-job-app/

docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/workflows:/workflows"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;SPRING_PROFILES_ACTIVE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/workflows/*.json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  registry.example.com/bytechef-runtime-job:2026-10-16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;daily-sales.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"reportDate": "2026-10-14"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connections&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONNECTIONS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image is about 2 GB (GraalVM base + a ~770 MB JAR with ~100 components and the polyglot runtime). Use immutable tags with &lt;code&gt;imagePullPolicy: IfNotPresent&lt;/code&gt; so each node pulls it once. Use the &lt;code&gt;prod&lt;/code&gt; profile; local builds default to &lt;code&gt;dev&lt;/code&gt;, which logs at DEBUG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes CronJob
&lt;/h2&gt;

&lt;p&gt;This is where it fits best: a pod starts, runs one workflow, exits, and the exit code decides whether Kubernetes retries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CronJob&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily-sales&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;concurrencyPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Forbid&lt;/span&gt;
  &lt;span class="na"&gt;jobTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;backoffLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
          &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runtime-job&lt;/span&gt;
              &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/bytechef-runtime-job:2026-10-16&lt;/span&gt;
              &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
              &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bin/sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
              &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
                  &lt;span class="s"&gt;exec java -jar /opt/bytechef/runtime-job/runtime-job-app.jar&lt;/span&gt;
                  &lt;span class="s"&gt;--workflow=daily-sales.json&lt;/span&gt;
                  &lt;span class="s"&gt;--parameters="{\"reportDate\": \"$(date -d yesterday +%F)\"}"&lt;/span&gt;
                  &lt;span class="s"&gt;--connections="$POSTGRES_CONNECTION"&lt;/span&gt;
                  &lt;span class="s"&gt;--timeout=20m&lt;/span&gt;
              &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SPRING_PROFILES_ACTIVE&lt;/span&gt;
                  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN&lt;/span&gt;
                  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/workflows/*.json&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_CONNECTION&lt;/span&gt;
                  &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily-sales-connections&lt;/span&gt;
                      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql.json&lt;/span&gt;
              &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;500m&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;768Mi&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
                &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1Gi&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;workflows&lt;/span&gt;
                  &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/workflows&lt;/span&gt;
          &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;workflows&lt;/span&gt;
              &lt;span class="na"&gt;configMap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily-sales-workflow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;restartPolicy: Never&lt;/code&gt; + &lt;code&gt;backoffLimit&lt;/code&gt;&lt;/strong&gt; — exit &lt;code&gt;1&lt;/code&gt; fails the pod; each retry is a clean process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--timeout&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;backoffLimit&lt;/code&gt; only retries &lt;em&gt;failed&lt;/em&gt; pods, not stuck ones. (&lt;code&gt;activeDeadlineSeconds&lt;/code&gt; works too, but it kills the pod with a silent &lt;code&gt;143&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; — a small workflow used ~370 MiB. Measure a real run and size from that; OOMKilled exits &lt;code&gt;137&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sh -c&lt;/code&gt; + &lt;code&gt;exec&lt;/code&gt;&lt;/strong&gt; — computes yesterday's date, and &lt;code&gt;exec&lt;/code&gt; keeps the JVM's exit code as the pod's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret + ConfigMap&lt;/strong&gt; — credentials stay out of the manifest; updating the workflow is a &lt;code&gt;kubectl apply&lt;/code&gt;, not an image rebuild.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CI
&lt;/h2&gt;

&lt;p&gt;Exit codes make it a normal CI step. GitHub Actions, generating release notes on every tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release notes&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/bytechef-runtime-job:2026-10-16&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate and publish release notes&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SPRING_PROFILES_ACTIVE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
          &lt;span class="na"&gt;BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workspace }}/.bytechef/*.json&lt;/span&gt;
          &lt;span class="na"&gt;OPENAI_CONNECTION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.BYTECHEF_OPENAI_CONNECTION }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;java -jar /opt/bytechef/runtime-job/runtime-job-app.jar \&lt;/span&gt;
            &lt;span class="s"&gt;--workflow=release-notes.json \&lt;/span&gt;
            &lt;span class="s"&gt;--parameters="{\"tag\": \"${GITHUB_REF_NAME}\"}" \&lt;/span&gt;
            &lt;span class="s"&gt;--connections="$OPENAI_CONNECTION" \&lt;/span&gt;
            &lt;span class="s"&gt;--timeout=10m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow lives in &lt;code&gt;.bytechef/&lt;/code&gt; in the repo it automates and gets code-reviewed like everything else. Same idea works in GitLab CI, Jenkins, or Airflow's &lt;code&gt;KubernetesPodOperator&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in, what's out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In:&lt;/strong&gt; ~100 components — HTTP client, script and Bash, helpers, CSV/JSON/XML/XLSX, PostgreSQL and MySQL, the major LLM providers, Google Workspace and Microsoft 365 basics, Slack, GitHub, Jira, HubSpot, Shopify — plus control flow (branch, condition, each, loop, map, parallel, fork-join, subflow, approval). Check the app's &lt;code&gt;build.gradle.kts&lt;/code&gt; before depending on a specific one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Out, on purpose:&lt;/strong&gt; triggers, the UI and REST API, execution history (ship your logs somewhere), and scaling within a run (launch more processes instead).&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Workflow with id: &amp;lt;base64&amp;gt; does not exist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;File isn't in a configured repository (or the volume isn't mounted). The directory in &lt;code&gt;--workflow&lt;/code&gt; is ignored.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unknown value for : &amp;lt;property&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Re-export the workflow from the editor.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth error on a task&lt;/td&gt;
&lt;td&gt;Connection key doesn't match task/component name, or property names are wrong.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process never exits&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;--timeout&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exit &lt;code&gt;143&lt;/code&gt;, nothing in log&lt;/td&gt;
&lt;td&gt;SIGTERM from outside. Check pod events.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exit &lt;code&gt;137&lt;/code&gt; / OOMKilled&lt;/td&gt;
&lt;td&gt;Raise the memory limit (and &lt;code&gt;-Xmx&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No outputs file&lt;/td&gt;
&lt;td&gt;The job didn't complete. Check the exit code.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Server or Runtime Job?
&lt;/h2&gt;

&lt;p&gt;Use the &lt;strong&gt;server&lt;/strong&gt; when you need triggers, webhooks, a UI, run history, RBAC, a central credential store, or always-on/high-frequency processing.&lt;/p&gt;

&lt;p&gt;Use the &lt;strong&gt;Runtime Job app&lt;/strong&gt; for nightly batches, CI steps, and anything whose schedule already lives in Kubernetes, Airflow or GitHub Actions.&lt;/p&gt;

&lt;p&gt;They're not competitors. A common setup: people build and test on a ByteChef server, and CronJobs run the exported, reviewed versions in production. Same JSON, unchanged.&lt;/p&gt;




&lt;p&gt;Full reference: &lt;a href="https://docs.bytechef.io" rel="noopener noreferrer"&gt;Runtime Job Runner docs&lt;/a&gt;. Originally published on the &lt;a href="https://blog.bytechef.io/blogs/bytechef-runtime-job" rel="noopener noreferrer"&gt;ByteChef blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>java</category>
    </item>
    <item>
      <title>Let Your Users Talk a Workflow Into Existence (ByteChef Embedded, Part 9)</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Fri, 16 Oct 2026 19:34:07 +0000</pubDate>
      <link>https://dev.to/bytechef/let-your-users-talk-a-workflow-into-existence-bytechef-embedded-part-9-3nf3</link>
      <guid>https://dev.to/bytechef/let-your-users-talk-a-workflow-into-existence-bytechef-embedded-part-9-3nf3</guid>
      <description>&lt;p&gt;Your users rarely know their automation up front. They discover it while they describe it: "when a deal closes... actually only enterprise deals... and loop in the account manager... and skip weekends."&lt;/p&gt;

&lt;p&gt;A template can't handle that. A drag-and-drop canvas scares half of them off. So in part nine of the ByteChef Embedded series, we give them a chat: they describe what they want, and ByteChef's workflow agent builds it in their account while they watch.&lt;/p&gt;

&lt;p&gt;On your side, it's one React component, one callback and a button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;EmbeddedWorkflowChat&lt;/code&gt; takes the connected user's JWT, starter suggestions, an optional system prompt, and an &lt;code&gt;onWorkflowReady&lt;/code&gt; callback.&lt;/li&gt;
&lt;li&gt;On mount it creates an empty draft workflow. The agent edits that draft directly, turn by turn.&lt;/li&gt;
&lt;li&gt;Every turn streams over &lt;a href="https://docs.ag-ui.com" rel="noopener noreferrer"&gt;AG-UI&lt;/a&gt; from &lt;code&gt;POST /automation/workflows/{uuid}/copilot/chat&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When the agent needs something, it renders a card in the thread: a question, an option picker, a connection picker, or a &lt;strong&gt;Connect&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt;After the first turn, &lt;code&gt;onWorkflowReady(uuid)&lt;/code&gt; fires and you can send the user to the editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where chat fits
&lt;/h2&gt;

&lt;p&gt;In the embedded sample app, open &lt;strong&gt;Automations&lt;/strong&gt;. The split button lists every way to start a workflow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fan09h3fplm844ih6ro4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fan09h3fplm844ih6ro4e.png" alt="Sample app Workflows page with the create menu open, listing New from Template, New from Embedded Workflow Builder, New from Prompt, New from Chat and New from Custom Workflow Builder" width="799" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;The user...&lt;/th&gt;
&lt;th&gt;Best when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New from Template (part 8)&lt;/td&gt;
&lt;td&gt;Picks a workflow you built&lt;/td&gt;
&lt;td&gt;You know the common automations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New from Embedded Workflow Builder&lt;/td&gt;
&lt;td&gt;Drags nodes on ByteChef's canvas&lt;/td&gt;
&lt;td&gt;The user likes visual builders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New from Prompt&lt;/td&gt;
&lt;td&gt;Writes one description, gets one result&lt;/td&gt;
&lt;td&gt;The request is short and clear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;New from Chat&lt;/strong&gt; (this part)&lt;/td&gt;
&lt;td&gt;Talks it through with an agent&lt;/td&gt;
&lt;td&gt;The request is fuzzy, or the user doesn't know the apps' vocabulary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New from Custom Workflow Builder (part 7)&lt;/td&gt;
&lt;td&gt;Fills in a UI you designed&lt;/td&gt;
&lt;td&gt;You want full control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Chat is the most forgiving option. Your users don't need to know Slack's action is called &lt;em&gt;Send Channel Message&lt;/em&gt; or that a schedule trigger has a &lt;code&gt;dayOfWeek&lt;/code&gt; property. The agent knows. When it isn't sure what they mean, it asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The page
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;New from Chat&lt;/strong&gt; and you land on &lt;code&gt;/automations/chat&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g7e4p4nblzx4i3dpltd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g7e4p4nblzx4i3dpltd.png" alt="New from Chat page with a collapsed System prompt (optional) field, the heading How can I help you today?, a message box and four suggestion chips" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A collapsed &lt;strong&gt;System prompt (optional)&lt;/strong&gt; field where you, the integrator, add house rules.&lt;/li&gt;
&lt;li&gt;The chat, centered while empty.&lt;/li&gt;
&lt;li&gt;Suggestion chips. Clicking one sends it as the first message, so a blank chat isn't intimidating.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the conversation starts, the composer docks to the bottom. After the first turn, an &lt;strong&gt;Open workflow&lt;/strong&gt; button appears top right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mounting the component
&lt;/h2&gt;

&lt;p&gt;Here's &lt;code&gt;app/automations/chat/page.tsx&lt;/code&gt;, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GenerateFromChatPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setJwtToken&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setWorkflowUuid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setJwtToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex h-screen w-full flex-col p-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mb-3 flex items-center justify-between gap-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 text-sm"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;System prompt (optional)&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Textarea&lt;/span&gt;
                        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                        &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"e.g. Always prefer Slack over email; keep workflows under five steps."&lt;/span&gt;
                        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

                &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/automations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open workflow&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"min-h-0 flex-1"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;jwtToken&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EmbeddedWorkflowChat&lt;/span&gt;
                        &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BYTECHEF_APP_BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BYTECHEF_ENVIRONMENT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                        &lt;span class="na"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;jwtToken&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                        &lt;span class="na"&gt;onWorkflowReady&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setWorkflowUuid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                        &lt;span class="na"&gt;suggestions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                            &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search for an action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;that can send an email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search for an action that can send an email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="p"&gt;},&lt;/span&gt;
                            &lt;span class="c1"&gt;// ...&lt;/span&gt;
                        &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                        &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getToken()&lt;/code&gt; asks the sample's back-end to sign a connected-user JWT, the same as in part one. The chat waits for the token, because everything it does is on behalf of that user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Props
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prop&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jwtToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;string&lt;/code&gt; (required)&lt;/td&gt;
&lt;td&gt;The connected user's JWT, sent as &lt;code&gt;Authorization: Bearer ...&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;baseUrl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your ByteChef instance. Defaults to &lt;code&gt;https://app.bytechef.io&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;`'DEVELOPMENT' \&lt;/td&gt;
&lt;td&gt;'STAGING' \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{% raw %}&lt;code&gt;suggestions&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{title, label, action}[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Starter chips. &lt;code&gt;action&lt;/code&gt; is the message that gets sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemPrompt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your house rules, applied to every turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onWorkflowReady&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(workflowUuid: string) =&amp;gt; void&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Called once, after the first turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;className&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extra classes for the container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The component fills its parent's height, so give the parent one (the sample uses &lt;code&gt;min-h-0 flex-1&lt;/code&gt; inside an &lt;code&gt;h-screen&lt;/code&gt; column).&lt;/p&gt;

&lt;h3&gt;
  
  
  It's source, not a package
&lt;/h3&gt;

&lt;p&gt;Unlike &lt;code&gt;useConnectDialog&lt;/code&gt; or &lt;code&gt;EmbeddedWorkflowBuilder&lt;/code&gt;, the chat isn't an npm export. It lives in the sample app under &lt;code&gt;front-end/src/components/embedded-workflow-chat/&lt;/code&gt;, and you copy the folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;embedded-workflow-chat/
├── index.ts
├── EmbeddedWorkflowChat.tsx           # the public component
├── EmbeddedCopilotRuntimeProvider.tsx # AG-UI agent and event handling
├── workflow-chat-thread.tsx           # messages, composer, chips
├── dataComponents.tsx                 # interactive cards
├── store.ts                           # zustand transcript store
├── api.ts, constants.ts               # skeleton workflow, endpoints
├── chat-config-context.tsx            # JWT, base URL, environment for cards
├── EmbeddedChatBoundary.tsx           # error boundary
└── assistant-ui/                      # markdown, tool card, reasoning renderers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's on purpose. A chat is the most visible thing in your product, so it should look like your product, not ours. It's built on &lt;a href="https://www.assistant-ui.com" rel="noopener noreferrer"&gt;assistant-ui&lt;/a&gt; primitives with Tailwind, so restyling is ordinary React work.&lt;/p&gt;

&lt;p&gt;Dependencies: &lt;code&gt;@ag-ui/client&lt;/code&gt;, &lt;code&gt;@assistant-ui/react&lt;/code&gt;, &lt;code&gt;@assistant-ui/react-markdown&lt;/code&gt;, &lt;code&gt;zustand&lt;/code&gt;, &lt;code&gt;lucide-react&lt;/code&gt;, &lt;code&gt;tailwind-merge&lt;/code&gt;, and &lt;code&gt;@bytechef/embedded&lt;/code&gt; for the ConnectDialog.&lt;/p&gt;

&lt;h2&gt;
  
  
  On mount: a skeleton workflow
&lt;/h2&gt;

&lt;p&gt;The agent doesn't generate a workflow and hand it back at the end. It edits a real one, in place, during the conversation. So before the user types anything, the component creates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SKELETON_WORKFLOW_DEFINITION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New Workflow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/embedded/v1/automation/workflows`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Environment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SKELETON_WORKFLOW_DEFINITION&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflowUuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The draft lands in the connected user's personal project for that environment. While the request runs, the component shows &lt;em&gt;Starting...&lt;/em&gt;. If it fails (expired JWT, wrong &lt;code&gt;baseUrl&lt;/code&gt;), the error shows in red instead of the chat.&lt;/p&gt;

&lt;p&gt;That UUID anchors the whole conversation. A ref latch makes sure only one skeleton is created, even under React StrictMode.&lt;/p&gt;

&lt;p&gt;One thing to plan for: every visit creates a draft, even if the user leaves without typing. More on that in Hardening.&lt;/p&gt;

&lt;h2&gt;
  
  
  On each turn: AG-UI streaming
&lt;/h2&gt;

&lt;p&gt;When the user sends a message, the component posts the conversation with an AG-UI &lt;code&gt;HttpAgent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;agentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workflow_editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/embedded/v1/automation/workflows/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/copilot/chat`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Environment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;trimmedSystemPrompt&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;additionalSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trimmedSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BUILD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BUILD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt; &lt;span class="nx"&gt;subscriber&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of that state, the server uses only the system prompt. It rebuilds everything else itself (see &lt;em&gt;What the agent can and can't do&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;The response is a &lt;code&gt;text/event-stream&lt;/code&gt;. The subscriber turns events into UI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AG-UI event&lt;/th&gt;
&lt;th&gt;What the user sees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TEXT_MESSAGE_START&lt;/code&gt;, &lt;code&gt;_CONTENT&lt;/code&gt;, &lt;code&gt;_END&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;An assistant message, streamed token by token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TOOL_CALL_START&lt;/code&gt;, &lt;code&gt;_ARGS&lt;/code&gt;, &lt;code&gt;_END&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A tool card (e.g. &lt;code&gt;searchTasks&lt;/code&gt;, &lt;code&gt;updateWorkflow&lt;/code&gt;) with its arguments filling in live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TOOL_CALL_RESULT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The card gets its result and a success or error icon. Interactive results add a card below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUN_ERROR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A red callout, e.g. &lt;em&gt;The copilot run failed. Reference: 3f2b...&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUN_FINISHED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The turn is over. The first time, &lt;code&gt;onWorkflowReady&lt;/code&gt; fires&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server closes the open text message whenever the agent calls a tool, so a turn reads in the order it happened: "Let me find the right trigger", the tool cards, then the next message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Errors users can act on
&lt;/h3&gt;

&lt;p&gt;When a run fails for a reason the user can do something about, ByteChef sends a fixed sentence plus the run ID:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;What the user reads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No AI provider for the environment&lt;/td&gt;
&lt;td&gt;No AI provider is set up for this environment, so the chat can't answer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;The AI provider is rate limiting requests right now. Try again in a minute.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overloaded&lt;/td&gt;
&lt;td&gt;The AI provider is overloaded right now. Try again in a minute.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad API key&lt;/td&gt;
&lt;td&gt;The AI provider rejected its API key. Ask an administrator to check it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context too long&lt;/td&gt;
&lt;td&gt;This conversation is too long for the AI model. Start a new chat.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anything else reads &lt;em&gt;The copilot run failed. Reference: ...&lt;/em&gt;. The text is fixed, never the exception message, because exceptions can name tables, internal hosts or keys. The full cause goes to the server log under the same run ID. When a user reports a failure, ask for the reference and grep for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The controls users expect
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stop&lt;/strong&gt; aborts the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit&lt;/strong&gt; on a user message rewinds and reruns from there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh&lt;/strong&gt; regenerates the last assistant message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy&lt;/strong&gt; and &lt;strong&gt;Export as Markdown&lt;/strong&gt; under assistant messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Edit and Refresh rewind the agent too, not just the screen. The agent's memory lives on the server, keyed to the user, the workflow and the conversation. Before each run, ByteChef trims that memory back to the last user message both sides agree on. The server only ever removes messages this way. It never takes the client's word for what was said.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interactive cards: when the agent needs the user
&lt;/h2&gt;

&lt;p&gt;A text-only agent would ask "which channel? please type its exact name" and hope. Instead, some tools return a small JSON envelope with a &lt;code&gt;kind&lt;/code&gt;, and the chat renders a card for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create-connection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"componentName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"componentLabel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Slack"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the agent looked up the trigger, then stopped to ask which time zone "9am" means:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jao8tk2zwuis87ojx4v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jao8tk2zwuis87ojx4v.png" alt="Embedded chat after the first prompt: getTaskDefinition and getTaskProperties tool cards, the agent asking for two pieces of info, and a Timezone question card with America/New_York, America/Chicago, America/Denver and America/Los_Angeles" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sample maps four kinds in &lt;code&gt;dataComponents.tsx&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;kind&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Card&lt;/th&gt;
&lt;th&gt;On click&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ask-user-question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multiple-choice questions, one at a time (&lt;em&gt;Question 1 of 3&lt;/em&gt;, &lt;em&gt;Previous&lt;/em&gt;)&lt;/td&gt;
&lt;td&gt;Answers go back as "User picked: ..."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;select-property-option&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Searchable list of valid values, like Slack channels or Google Sheets&lt;/td&gt;
&lt;td&gt;The label and real value go back, and the agent writes the value into the workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;select-connection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Searchable list of the user's connections for an app&lt;/td&gt;
&lt;td&gt;Name and ID go back. With one connection, it's picked automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create-connection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A &lt;strong&gt;Connect Slack&lt;/strong&gt; button&lt;/td&gt;
&lt;td&gt;Opens the ConnectDialog. When done, the card shows &lt;strong&gt;Connected Slack&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of them do nothing on the server except hand the question to the browser. The property picker does a bit more: it fetches the property's real options first, using a connection the user may use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9xsmonuihkky2lm6vsfi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9xsmonuihkky2lm6vsfi.png" alt="Embedded chat stopped at a searchable Slack channel picker listing random, general, testing, sales-team and more" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the agent shows a card, its turn ends. It doesn't guess and carry on. It waits. The user's pick goes back as an ordinary message, which starts the next turn with exactly what the agent asked for. Answered cards collapse to a short summary with a green check (&lt;em&gt;Picked: #standup&lt;/em&gt;), so the thread stays readable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect, right in the conversation
&lt;/h3&gt;

&lt;p&gt;This is where the whole series comes together. The user asks for a Slack reminder but hasn't connected Slack. The agent calls &lt;code&gt;createConnection&lt;/code&gt;, and the chat shows &lt;strong&gt;Connect Slack&lt;/strong&gt;. Clicking it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calls &lt;code&gt;GET /api/embedded/v1/integrations&lt;/code&gt; with the user's JWT and finds the integration whose &lt;code&gt;componentName&lt;/code&gt; is &lt;code&gt;slack&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Opens the ConnectDialog from part one via &lt;code&gt;useConnectDialog&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Watches the dialog's portal and marks the card connected when it closes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The user goes through the same OAuth or API-key flow as on your Integrations page, without leaving the chat. Then the agent picks the new connection with &lt;code&gt;select-connection&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;createConnection&lt;/code&gt; is signaling only. The agent never sees or stores credentials, and the tool call creates nothing on the server. If the app isn't one of your integrations, the card says &lt;em&gt;No integration is available for Slack&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your house rules: the system prompt
&lt;/h2&gt;

&lt;p&gt;The user decides what the workflow does. You decide how it's built.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdm694kpr1s88oz13dgiw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdm694kpr1s88oz13dgiw.png" alt="New from Chat page with the System prompt open: Always notify the team in Slack, never by email. Keep workflows under five steps." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Good system prompts encode decisions your users shouldn't have to make:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always notify the team in Slack, never by email.&lt;/li&gt;
&lt;li&gt;Keep workflows under five steps.&lt;/li&gt;
&lt;li&gt;Use the Europe/Berlin time zone for every schedule unless the user names another.&lt;/li&gt;
&lt;li&gt;Never delete CRM records; update a status field instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How ByteChef treats it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read every turn.&lt;/strong&gt; Change it mid-conversation and the next turn picks it up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trimmed and capped&lt;/strong&gt; at 4,000 characters on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can't override the core rules.&lt;/strong&gt; It's appended under a heading that marks it as additional instructions from the integrating application, applied only where it doesn't conflict with the build rules, the workflow-definition contract or safety constraints. Your rules steer the agent. They can't turn it into something else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the sample it's a text box so you can experiment. In production it belongs in your code: build it from your product's conventions and what you know about the user (plan, time zone, approved apps).&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent can and can't do
&lt;/h2&gt;

&lt;p&gt;The chat is a public surface. It runs with the connected user's JWT, and the browser controls the request body. So the copilot endpoint trusts none of the AG-UI state it receives. On every turn, ByteChef:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checks the workflow belongs to the user.&lt;/strong&gt; The &lt;code&gt;{workflowUuid}&lt;/code&gt; must be in the user's own project for that environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuilds the agent state from scratch.&lt;/strong&gt; It keeps only the additional system prompt, then sets the rest itself: BUILD mode, non-autonomous, the verified workflow ID, the user's project, the environment from &lt;code&gt;X-Environment&lt;/code&gt; (&lt;code&gt;PRODUCTION&lt;/code&gt; if missing), and a conversation key built from tenant, user and workflow. Editing the request can't switch modes, change workspaces or read someone else's conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeps the agent inside the user's project.&lt;/strong&gt; Every workflow tool checks ownership, and project-level changes are off limits. Talk the model into opening another workflow and the tool refuses: &lt;em&gt;"This chat can only work with the projects and workflows of its own project."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allows only the user's own connections.&lt;/strong&gt; The agent sees only connections the user owns or was given, and every connection ID it binds or queries is checked against that list: &lt;em&gt;"This chat can only use connections its user is entitled to use."&lt;/em&gt; No borrowing another customer's Slack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits the agent to your catalog.&lt;/strong&gt; It can use only enabled integrations for that environment, plus connection-free components (schedules, conditions, loops, data mappers). Anything else doesn't show up in search, can't be looked up, and is rejected anywhere in a saved workflow, including inside conditions, loops and AI agent nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is easy to miss. If you offer HubSpot and Slack but not Salesforce, a user asking for "Salesforce to Airtable" gets a conversation about what's possible, not a workflow that could never run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftgc1xb5io3jq0jlnd6xu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftgc1xb5io3jq0jlnd6xu.png" alt="The agent explains Salesforce and Airtable aren't available, suggests a webhook or a schedule with the HTTP client instead, and asks which approach to take" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing off the result
&lt;/h2&gt;

&lt;p&gt;The chat produces a real ByteChef workflow, so it's not a dead end. After the first turn, the component calls &lt;code&gt;onWorkflowReady&lt;/code&gt; with the UUID from mount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EmbeddedWorkflowChat&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="na"&gt;onWorkflowReady&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setWorkflowUuid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/automations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open workflow&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the build is done, the agent says what it made:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb8syk9p5v1lhu5mww25n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb8syk9p5v1lhu5mww25n.png" alt="End of the build turn: validateWorkflow, updateWorkflow and saveWorkflowTestConnection tool cards, then a summary: an Every Day trigger Monday to Friday at 9:00 AM America/New_York and a Send Channel Message action posting Standup in 5 minutes to #general" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things about the callback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It fires once, after the first turn, not when the workflow is "done".&lt;/strong&gt; That turn may have built three steps or only asked a question. The UUID never changes and the agent keeps updating the same workflow, so opening it later always shows the latest. Read it as "there's a workflow worth linking to now".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a callback, so you decide what it means.&lt;/strong&gt; A button, a live preview next to the chat, a Save button, an analytics event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;/automations/{uuid}&lt;/code&gt; renders the same &lt;code&gt;EmbeddedWorkflowBuilder&lt;/code&gt; from part eight. Chat to draft, canvas to polish.&lt;/p&gt;

&lt;h2&gt;
  
  
  From draft to running
&lt;/h2&gt;

&lt;p&gt;A chat-built workflow is a draft. Nothing runs until it's published and enabled, which is the right default for something an AI just wrote. The calls from part seven take it live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Freeze the current definition as the first version&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/embedded/v1/automation/workflows/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/publish`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Created from chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Turn it on (DELETE to turn it off)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/embedded/v1/automation/workflows/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/enable`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the sample the user does this by hand: &lt;strong&gt;Publish&lt;/strong&gt; in the editor, then the toggle on the Automations list. A more guided product could add an &lt;strong&gt;Activate&lt;/strong&gt; button under the chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Your ByteChef instance needs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The copilot enabled:&lt;/strong&gt; &lt;code&gt;bytechef.ai.copilot.enabled=true&lt;/code&gt;. Without it, &lt;code&gt;/copilot/chat&lt;/code&gt; doesn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An AI provider configured.&lt;/strong&gt; The run's environment decides which settings apply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrations enabled for the environment.&lt;/strong&gt; With none, the agent can still build schedules and logic, but nothing that talks to an app.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Embedded workflows and the copilot endpoint are Enterprise Edition features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardening the demo for production
&lt;/h2&gt;

&lt;p&gt;The sample is intentionally small. Before you ship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clean up abandoned drafts.&lt;/strong&gt; Create the skeleton on the first message, or &lt;code&gt;DELETE /automation/workflows/{uuid}&lt;/code&gt; untouched drafts when the user leaves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write suggestions for building.&lt;/strong&gt; The sample's chips come from ByteChef's editor copilot, where a workflow already exists. On an empty one, try "Post a Slack message every Monday at 9:00" or "When a HubSpot deal closes, create a Jira ticket".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own the system prompt.&lt;/strong&gt; Generate it in code. Don't hand end users a text box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass &lt;code&gt;environment&lt;/code&gt; explicitly.&lt;/strong&gt; It defaults to &lt;code&gt;PRODUCTION&lt;/code&gt;, and a JWT signed for another environment is rejected when the skeleton is created.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide what "ready" means.&lt;/strong&gt; For a stricter signal (a trigger exists, no open issues), fetch the workflow when the callback fires and check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support multi-select questions.&lt;/strong&gt; The sample renders every question as single-select and ignores the &lt;code&gt;multiSelect&lt;/code&gt; flag for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the error boundary.&lt;/strong&gt; &lt;code&gt;EmbeddedChatBoundary&lt;/code&gt; hides a harmless assistant-ui unmount error, but renders nothing for any other crash. Replace that &lt;code&gt;null&lt;/code&gt; with a fallback and report the error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a New chat button.&lt;/strong&gt; Remount the component (change its &lt;code&gt;key&lt;/code&gt;) to get a fresh skeleton, a new conversation and an empty agent memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surface the error reference.&lt;/strong&gt; Make the run ID easy to copy and teach support to search logs for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who owns what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You own&lt;/strong&gt; where the chat appears, how it looks (it's your source code), the suggestions, the system prompt, which integrations exist, and what happens when a workflow is ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ByteChef owns&lt;/strong&gt; the agent and its tools, the AG-UI stream, component search and property lookups, validation, per-user storage, the security boundary around every turn, the ConnectDialog and credentials, and the engine that runs the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your code is one component, one callback and a button. Behind it is an agent that knows every action in your catalog and can turn "remind my team about standup every weekday" into a scheduled Slack workflow on the user's own account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The series so far
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Connect your users' apps (the JWT and ConnectDialog everything rests on)&lt;/li&gt;
&lt;li&gt;Act on connected apps directly with the ComponentKit&lt;/li&gt;
&lt;li&gt;Chat with an AI assistant that uses the ComponentKit's tools&lt;/li&gt;
&lt;li&gt;Trigger workflows from your product's events&lt;/li&gt;
&lt;li&gt;Call a workflow like a synchronous API&lt;/li&gt;
&lt;li&gt;Agent over MCP: the same tools, discovered through an open protocol&lt;/li&gt;
&lt;li&gt;Author workflows in a builder you designed&lt;/li&gt;
&lt;li&gt;Start from a template you curated&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Talk a workflow into existence (this part)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Parts three and nine both put an AI chat in your product, but they do opposite jobs. The ComponentKit chat acts on the user's apps right now: it sends the email, creates the ticket. New from Chat builds something that acts later, on its own, every time the trigger fires. One is an assistant. The other is an automation author.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; enable the copilot and an AI provider in ByteChef, run the embedded sample app, and open &lt;strong&gt;Automations → New from Chat&lt;/strong&gt;. Ask for a weekday Slack reminder and watch it build.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.bytechef.io/blogs/embedded-chat" rel="noopener noreferrer"&gt;ByteChef blog&lt;/a&gt;. ByteChef is an open-source workflow automation and AI agent platform: &lt;a href="https://github.com/bytechefhq/bytechef" rel="noopener noreferrer"&gt;github.com/bytechefhq/bytechef&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Add a "New from Template" Gallery to Your SaaS in Two API Calls</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Sun, 11 Oct 2026 15:44:35 +0000</pubDate>
      <link>https://dev.to/bytechef/add-a-new-from-template-gallery-to-your-saas-in-two-api-calls-15nj</link>
      <guid>https://dev.to/bytechef/add-a-new-from-template-gallery-to-your-saas-in-two-api-calls-15nj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A "New from Template" gallery in your SaaS takes &lt;strong&gt;two REST calls&lt;/strong&gt; with ByteChef Embedded: &lt;code&gt;GET /automation/projects&lt;/code&gt; returns your curated catalog, and &lt;code&gt;POST /automation/workflow-templates/{uuid}/copy&lt;/code&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most users don't want a blank canvas. In &lt;a href="https://blog.bytechef.io/blogs/embedded-custom-builder" rel="noopener noreferrer"&gt;part 7&lt;/a&gt; we let users build workflows from scratch in a UI we designed — flexible, but slow. What users actually want to hear is: &lt;em&gt;"Here's an automation we already built for you. Want it?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why templates get the big button
&lt;/h2&gt;

&lt;p&gt;In the embedded sample app, &lt;strong&gt;New from Template&lt;/strong&gt; is the primary action on the Automations page. Every other option lives in the split menu:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fan09h3fplm844ih6ro4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fan09h3fplm844ih6ro4e.png" alt="Sample app create menu: New from Template, New from Embedded Workflow Builder, New from Prompt, New from Chat, New from Custom Workflow Builder" width="799" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reason is simple: every other option asks the user to &lt;em&gt;describe&lt;/em&gt; an automation (on a canvas, in a prompt, in a form). A template only asks them to &lt;em&gt;pick&lt;/em&gt; one. For onboarding, that's the shortest path from "I just signed up" to "something is running for me."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the user sees
&lt;/h2&gt;

&lt;p&gt;Clicking the button opens &lt;code&gt;/automations/templates&lt;/code&gt;: one section per project, one card per template.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmsgfex395rriykwdhob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmsgfex395rriykwdhob.png" alt="Choose a workflow template page with Automation Playground and Team productivity sections" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything on a card comes from ByteChef:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the template's &lt;strong&gt;label&lt;/strong&gt; (&lt;em&gt;Daily standup reminder&lt;/em&gt;),&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;description&lt;/strong&gt; you wrote,&lt;/li&gt;
&lt;li&gt;a row of &lt;strong&gt;component icons&lt;/strong&gt; for the apps it uses (here: Slack).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Build a template in ByteChef
&lt;/h2&gt;

&lt;p&gt;Templates live in ByteChef, not in your app. Let's build &lt;em&gt;Daily standup reminder&lt;/em&gt;: every weekday at 9:00, post a reminder to Slack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open Automation Workflows
&lt;/h3&gt;

&lt;p&gt;In ByteChef go to &lt;strong&gt;Embedded → Automations&lt;/strong&gt;. You'll see your catalog projects, each with a version badge:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgzqzi6d9dbag73wmnbb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgzqzi6d9dbag73wmnbb.png" alt="ByteChef Automation Workflows page with two published projects" width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This page only exists in the &lt;strong&gt;Development&lt;/strong&gt; environment, so you author templates in one place. The &lt;em&gt;published&lt;/em&gt; catalog, however, is served to connected users in every environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a project
&lt;/h3&gt;

&lt;p&gt;A project is a shelf in your gallery. Click &lt;strong&gt;New Project&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fupspd71sjhxhezw2gxr8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fupspd71sjhxhezw2gxr8.png" alt="Create Project dialog for Team productivity" width="512" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt; and &lt;strong&gt;Description&lt;/strong&gt; become the section heading and subheading — write them for customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Category&lt;/strong&gt; and &lt;strong&gt;Tags&lt;/strong&gt; only organize this admin page; they're not in the API response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission Expression&lt;/strong&gt; limits who sees the whole project (more below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show in Automation Hub&lt;/strong&gt; controls ByteChef's own embeddable Hub. Turn it off for flows you only expose through the API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save, and the project shows up with a &lt;code&gt;DRAFT&lt;/code&gt; badge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a workflow
&lt;/h3&gt;

&lt;p&gt;Each workflow = one template card. Click &lt;strong&gt;+ Workflow&lt;/strong&gt; on the project row:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbeyrp8xfw2vb4hbcy2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbeyrp8xfw2vb4hbcy2t.png" alt="Create Workflow dialog for Daily standup reminder" width="512" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Now build it — on the canvas, or by pasting a definition into the &lt;strong&gt;Workflow Code Editor&lt;/strong&gt; (the &lt;code&gt;&amp;lt;/&amp;gt;&lt;/code&gt; button):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx7iu49s5zfcnzvbe9kt6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx7iu49s5zfcnzvbe9kt6.png" alt="Workflow code editor with a schedule trigger and a Slack sendChannelMessage task" width="799" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a &lt;code&gt;schedule/v1/everyDay&lt;/code&gt; trigger (9:00, &lt;code&gt;dayOfWeek&lt;/code&gt; 2–6) plus one Slack &lt;strong&gt;Send Channel Message&lt;/strong&gt; task. Close the editor and ByteChef flags two issues:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2g4j261c53u1gcdhguwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2g4j261c53u1gcdhguwj.png" alt="Editor with Missing required property: channel and Missing required connection: Slack" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leave them.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Publish
&lt;/h3&gt;

&lt;p&gt;Templates are invisible until the project is published. Open the project's &lt;code&gt;⋮&lt;/code&gt; menu and click &lt;strong&gt;Publish&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkoz973hfqxkrl7oljwoe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkoz973hfqxkrl7oljwoe.png" alt="Project menu with Publish, New Workflow, Import Workflow, Edit and Delete" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No confirmation, no validation gate — the badge just flips from &lt;code&gt;DRAFT&lt;/code&gt; to &lt;code&gt;V1 PUBLISHED&lt;/code&gt;. A few things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users always get the &lt;strong&gt;latest published version&lt;/strong&gt;, never your work in progress. Edit freely; nothing changes until you publish V2.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;unpublished project still appears in the API, but empty&lt;/strong&gt; — so publish before you tell customers to look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copies don't follow new versions.&lt;/strong&gt; V2 only affects new copies. (References behave differently — see below.)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 2: Fetch the catalog
&lt;/h2&gt;

&lt;p&gt;The gallery loads everything with one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchAutomationWorkflowProjects&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AutomationWorkflowProject&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/embedded/v1/automation/projects&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed to fetch automation workflow projects: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fetchWithAuth&lt;/code&gt; sends the connected user's JWT (from &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;part 1&lt;/a&gt;) plus an &lt;code&gt;X-Environment&lt;/code&gt; header (defaults to &lt;code&gt;PRODUCTION&lt;/code&gt;). The JWT matters here because &lt;strong&gt;the catalog is filtered per user&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The response looks like this (icon shortened):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Team productivity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ready-made workflows that keep your team in sync"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"automationHubVisible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"workflowTemplates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6a62a705-2225-445d-a91a-048607332571"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Daily standup reminder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Every weekday at 9:00, remind your team in Slack that standup is about to start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"components"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Slack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;svg ...&amp;gt;...&amp;lt;/svg&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Section heading + subheading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automationHubVisible&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The Hub switch. Not used for filtering — a hint for your UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kind&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;COPY&lt;/code&gt; (normal projects) or &lt;code&gt;REFERENCE&lt;/code&gt; (code-native projects)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workflowTemplates[].id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The UUID you pass to the copy call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workflowTemplates[].components&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apps used: &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, SVG &lt;code&gt;icon&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workflowTemplates[].inputs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Declared workflow inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two gotchas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;components&lt;/code&gt; lists tasks, not the trigger.&lt;/strong&gt; A "New HubSpot deal → Slack" template would only show Slack. If the trigger app matters, mention it in the label or description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;icon&lt;/code&gt; is raw SVG, not a URL.&lt;/strong&gt; No hosting or proxying needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`data:image/svg+xml;utf8,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"size-full"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Who sees which template
&lt;/h3&gt;

&lt;p&gt;Projects and workflows can each carry a &lt;strong&gt;Permission Expression&lt;/strong&gt; — a SpEL condition evaluated against the connected user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metadata['plan'] == 'pro'
metadata['tier'] == 'enterprise'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expressions can read the user's &lt;code&gt;metadata&lt;/code&gt;, &lt;code&gt;externalId&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;environment&lt;/code&gt;. That gives you tiered catalogs with zero app code.&lt;/p&gt;

&lt;p&gt;The rules are fail-closed: empty means everyone; otherwise it must evaluate to &lt;code&gt;true&lt;/code&gt;. A &lt;code&gt;false&lt;/code&gt; or a broken expression &lt;strong&gt;hides&lt;/strong&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Render the gallery
&lt;/h2&gt;

&lt;p&gt;This part is plain React. The sample's &lt;code&gt;app/automations/templates/page.tsx&lt;/code&gt; keeps four bits of state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setProjects&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AutomationWorkflowProject&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;creatingWorkflowId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCreatingWorkflowId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; per project, one shadcn/ui &lt;code&gt;Card&lt;/code&gt; per template, in a 1–3 column grid. While a copy is in flight, the clicked card reads &lt;em&gt;Creating workflow...&lt;/em&gt; and every card gets &lt;code&gt;aria-disabled&lt;/code&gt;, so a double click can't start two copies.&lt;/p&gt;

&lt;p&gt;Nothing here is ByteChef-specific. Cards, a searchable list, an onboarding checklist — the API gives you data, not UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: One click, one copy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSelectWorkflowTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflowTemplateUuid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setCreatingWorkflowId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflowTemplateUuid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;copyWorkflowTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflowTemplateUuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/automations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to create a workflow from the selected template&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setCreatingWorkflowId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;copyWorkflowTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`/api/embedded/v1/automation/workflow-templates/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/copy`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed to copy workflow template: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newWorkflowUuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// The endpoint may return the uuid as a JSON string or as plain text.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;newWorkflowUuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;newWorkflowUuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;newWorkflowUuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newWorkflowUuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one &lt;code&gt;POST&lt;/code&gt; does a lot:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Copies the latest published version&lt;/strong&gt; from the user's permission-filtered catalog. Drafts never leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeps the label and description&lt;/strong&gt; — no "(Copy)" suffix. To the user, it's just &lt;em&gt;their&lt;/em&gt; automation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lands in the user's own project&lt;/strong&gt;, auto-created per user and environment. Your app never manages projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remembers its origin&lt;/strong&gt; and allows &lt;strong&gt;one copy per template per user&lt;/strong&gt;. A second click fails — in production, show an &lt;em&gt;Added&lt;/em&gt; badge that opens the existing copy instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the user gets
&lt;/h2&gt;

&lt;p&gt;The sample routes to &lt;code&gt;/automations/{uuid}&lt;/code&gt;, which renders ByteChef's embedded editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EmbeddedWorkflowBuilder&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BYTECHEF_APP_BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;connectionDialogAllowed&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BYTECHEF_ENVIRONMENT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;jwtToken&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a copy of the &lt;em&gt;Greeting Reference&lt;/em&gt; template, now in the user's workspace:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmym1wq2ppnyp8ewovyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmym1wq2ppnyp8ewovyk.png" alt="Embedded editor showing the copied Greeting Reference workflow marked V1 DRAFT" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note the &lt;strong&gt;&lt;code&gt;V1 DRAFT&lt;/code&gt;&lt;/strong&gt; 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 &lt;em&gt;Daily standup reminder&lt;/em&gt; arrives with the two open issues we left in, so the user's first job is choosing their Slack connection and channel.)&lt;/p&gt;

&lt;p&gt;Back on the Automations list, the new copy sits at the top as &lt;code&gt;DRAFT&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5u3u0mmnasjozb0wu5b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5u3u0mmnasjozb0wu5b.png" alt="Workflows list with the copied workflow at the top marked DRAFT and a provisioned reference at the bottom" width="799" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The row at the bottom came from the &lt;em&gt;same&lt;/em&gt; template — but as a &lt;strong&gt;reference&lt;/strong&gt;, not a copy. More on that in a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  From draft to running
&lt;/h2&gt;

&lt;p&gt;Same three calls as part 7, authenticated with the user's JWT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Bind the user's connection to each node that needs one&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`/api/embedded/v1/automation/workflows/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/workflow-nodes/slack_1/connection/slack`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;connectionId&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Freeze the current definition as V1&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/embedded/v1/automation/workflows/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/publish`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Created from template&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Turn it on (DELETE to turn it off)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/embedded/v1/automation/workflows/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/enable`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;strong&gt;"Activate" wizard&lt;/strong&gt;: pick a Slack account from &lt;code&gt;GET /components/slack/connections&lt;/code&gt;, fill any inputs, then bind → publish → enable. That's exactly what ByteChef's own Automation Hub does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy or reference?
&lt;/h2&gt;

&lt;p&gt;There are two ways to hand a user a catalog workflow. The difference is &lt;em&gt;who owns it afterwards&lt;/em&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Copy&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Reference&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Call&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST .../{uuid}/copy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST .../{uuid}/provision&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User gets&lt;/td&gt;
&lt;td&gt;A new, independent workflow&lt;/td&gt;
&lt;td&gt;A per-user deployment of &lt;em&gt;your&lt;/em&gt; workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editable?&lt;/td&gt;
&lt;td&gt;Yes, it's theirs&lt;/td&gt;
&lt;td&gt;No, it runs your definition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You publish V2&lt;/td&gt;
&lt;td&gt;Nothing changes (snapshot)&lt;/td&gt;
&lt;td&gt;Every reference rolls forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connections&lt;/td&gt;
&lt;td&gt;Bound per node after copy&lt;/td&gt;
&lt;td&gt;Auto-wired or passed in the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial state&lt;/td&gt;
&lt;td&gt;Draft&lt;/td&gt;
&lt;td&gt;Enabled once connections/inputs resolve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Copy&lt;/strong&gt; when the template is a starting point users will customize. &lt;strong&gt;Reference&lt;/strong&gt; when the workflow is really part of your product and you want to ship fixes once to everyone.&lt;/p&gt;

&lt;p&gt;Check &lt;code&gt;kind&lt;/code&gt;: &lt;code&gt;COPY&lt;/code&gt; projects support both; &lt;code&gt;REFERENCE&lt;/code&gt; (code-native) projects can only be provisioned — copying returns &lt;code&gt;409&lt;/code&gt; with &lt;code&gt;{"reason": "CODE_WORKFLOW_NOT_COPYABLE"}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-to-server variants
&lt;/h2&gt;

&lt;p&gt;Every call has a backend form authenticated with your environment's API secret key:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser (JWT)&lt;/th&gt;
&lt;th&gt;Backend (API key)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /automation/projects&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /{externalUserId}/automation/projects&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /automation/workflow-templates/{uuid}/copy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /{externalUserId}/automation/workflow-templates/{uuid}/copy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Respect &lt;code&gt;kind&lt;/code&gt;&lt;/strong&gt; — hide &lt;code&gt;REFERENCE&lt;/code&gt; templates or route them to &lt;code&gt;/provision&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Track what the user already has&lt;/strong&gt; — show &lt;em&gt;Added / Open&lt;/em&gt; instead of letting a second copy fail.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Surface the trigger's app&lt;/strong&gt; — &lt;code&gt;components&lt;/code&gt; only lists tasks.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Decide on &lt;code&gt;automationHubVisible&lt;/code&gt;&lt;/strong&gt; — the API returns hidden projects too.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Distinguish errors&lt;/strong&gt; — "template no longer available" (404) vs. transient failures.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Offer activation, not just a copy&lt;/strong&gt; — a two-step wizard gets users to a running automation in under a minute.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who owns what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You own curation:&lt;/strong&gt; which workflows exist, how they're grouped, who sees them, when they change, and how the gallery looks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ByteChef owns the machinery:&lt;/strong&gt; the editor, versioning, per-user filtering, tenant-isolated copies, component metadata and icons, connection storage, and the engine that runs it all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your code for the entire feature: &lt;strong&gt;two API calls and a grid of cards.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The series so far
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;Connect&lt;/a&gt; your users' apps (JWT + ConnectDialog)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit" rel="noopener noreferrer"&gt;Act&lt;/a&gt; on connected apps with the ComponentKit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;Chat&lt;/a&gt; with an AI assistant using ComponentKit tools&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.bytechef.io/blogs/embedded-app-events" rel="noopener noreferrer"&gt;Trigger&lt;/a&gt; workflows from your product's events&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.bytechef.io/blogs/embedded-request" rel="noopener noreferrer"&gt;Call&lt;/a&gt; a workflow like a synchronous API&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.bytechef.io/blogs/embedded-mcp-chat" rel="noopener noreferrer"&gt;Agents over MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.bytechef.io/blogs/embedded-custom-builder" rel="noopener noreferrer"&gt;Author&lt;/a&gt; workflows in your own custom builder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start from a curated template&lt;/strong&gt; (this post)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; in ByteChef, open &lt;strong&gt;Embedded → Automations&lt;/strong&gt;, create a project with a workflow or two, and &lt;strong&gt;Publish&lt;/strong&gt;. Then run the embedded sample app, open &lt;strong&gt;Automations → New from Template&lt;/strong&gt;, and click a card.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.bytechef.io/blogs/embedded-templates" rel="noopener noreferrer"&gt;ByteChef blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>automation</category>
      <category>saas</category>
    </item>
    <item>
      <title>ByteChef Embedded, Part 7: New from Custom Workflow Builder</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:34:39 +0000</pubDate>
      <link>https://dev.to/bytechef/bytechef-embedded-part-7-new-from-custom-workflow-builder-4m4g</link>
      <guid>https://dev.to/bytechef/bytechef-embedded-part-7-new-from-custom-workflow-builder-4m4g</guid>
      <description>&lt;p&gt;Most embedded workflow tools give you two choices: embed their editor, or don't offer automations at all. There's a third option. Build the editor yourself, and let the platform run whatever it produces.&lt;/p&gt;

&lt;p&gt;This post walks through the Custom Workflow Builder in the ByteChef embedded sample app. It's a small React page that lets users build "when X happens, if Y, do Z" rules in your own UI. ByteChef never draws a pixel of it. It only sees the JSON the page produces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Builder state lives in plain React &lt;code&gt;useState&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A pure function renders that state into ByteChef's workflow-definition JSON on every keystroke.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /automation/workflows&lt;/code&gt; creates the workflow in the connected user's project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PUT .../workflow-nodes/{node}/connection/{component}&lt;/code&gt; binds each node to the user's own connection.&lt;/li&gt;
&lt;li&gt;The result is an ordinary ByteChef workflow. You can open it in ByteChef's editor, publish it, enable it, and it runs on ByteChef's engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not just embed the full editor?
&lt;/h2&gt;

&lt;p&gt;ByteChef's own editor can be embedded, and it's powerful: canvas, hundreds of components, data pills, loops, branches, a test runner.&lt;/p&gt;

&lt;p&gt;That's exactly what some products don't want. If your users are "set up a rule" people rather than "build a flow" people, you want a narrow, opinionated form in your design system:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When an email arrives, if the subject says invoice, post to #finance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Custom Workflow Builder shows how small that layer can be.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fan09h3fplm844ih6ro4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fan09h3fplm844ih6ro4e.png" alt="Sample app create menu listing New from Template, New from Embedded Workflow Builder, New from Prompt, New from Chat and New from Custom Workflow Builder" width="799" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The builder in three files
&lt;/h2&gt;

&lt;p&gt;No graph library, no drag and drop, no canvas. Just form cards stacked top to bottom: a Trigger card, then steps you add with &lt;strong&gt;Add Action&lt;/strong&gt; and &lt;strong&gt;Add Condition&lt;/strong&gt;. A live panel on the right shows the exact JSON the page will send.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe825f2uvj6dy432c10tf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe825f2uvj6dy432c10tf.png" alt="Custom Workflow Builder with a Gmail trigger, a condition and a Slack action next to the live workflow-definition JSON" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which components, triggers, actions and fields your builder offers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;definition.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Builder state types, plus a pure function that renders state into ByteChef JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;page.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The UI, plus the save flow that calls the embedded API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's follow the data through each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The catalog is your product surface
&lt;/h2&gt;

&lt;p&gt;ByteChef has hundreds of components. Your builder doesn't have to offer them. &lt;code&gt;catalog.ts&lt;/code&gt; is a hardcoded list of exactly what this product supports, Gmail and Slack, with only the fields worth asking for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COMPONENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CatalogComponent&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;googleMail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newEmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]}],&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sendEmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Send Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;To (comma-separated)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stringArray&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Subject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bodyType&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TEXT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Slack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anyEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Any Event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]}],&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* sendChannelMessage, sendDirectMessage, addReaction */&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things make this more than a list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;name&lt;/code&gt; values are ByteChef's.&lt;/strong&gt; &lt;code&gt;googleMail&lt;/code&gt;, &lt;code&gt;newEmail&lt;/code&gt;, &lt;code&gt;sendEmail&lt;/code&gt; end up in each node's &lt;code&gt;type&lt;/code&gt;. Labels can say anything. Names must match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;key&lt;/code&gt; values are ByteChef property names.&lt;/strong&gt; &lt;code&gt;to&lt;/code&gt;, &lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt; are Send Email's real inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kind&lt;/code&gt; controls serialization.&lt;/strong&gt; &lt;code&gt;stringArray&lt;/code&gt; turns &lt;code&gt;"a@x.com, b@x.com"&lt;/code&gt; into an array. &lt;code&gt;hidden&lt;/code&gt; never shows in the form and always sends a fixed value, like &lt;code&gt;bodyType: "TEXT"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where your product's opinions live. Hide delete actions. Fix the email format. Rename "Send Channel Message" to "Notify the team". ByteChef doesn't need to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Keep state in your shape
&lt;/h2&gt;

&lt;p&gt;The example workflow: &lt;strong&gt;Route invoice emails&lt;/strong&gt;. When a new Gmail message arrives, post it to Slack if the subject mentions an invoice, otherwise send an auto-reply.&lt;/p&gt;

&lt;p&gt;The condition compares &lt;code&gt;${trigger_1.subject}&lt;/code&gt; against &lt;code&gt;invoice&lt;/code&gt; with &lt;code&gt;contains&lt;/code&gt;. That &lt;code&gt;${...}&lt;/code&gt; is ByteChef's reference syntax. &lt;code&gt;trigger_1&lt;/code&gt; is the trigger node's name, &lt;code&gt;subject&lt;/code&gt; is a field the Gmail trigger outputs. The builder doesn't evaluate any of it. It passes strings through, and ByteChef resolves them at run time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgcb5x8z8xnlfbwsyzbg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgcb5x8z8xnlfbwsyzbg.png" alt="Condition card checking if the subject contains invoice, with a Slack action under If true and a Gmail action under If false" width="548" height="1106"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All of it lives in one &lt;code&gt;useState&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;BuilderState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;TriggerState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// componentName, operation, parameters, connectionId&lt;/span&gt;
  &lt;span class="nl"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StepState&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;     &lt;span class="c1"&gt;// ActionStepState | ConditionStepState (caseTrue / caseFalse)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state is your shape, not ByteChef's. Connection IDs sit next to parameters, steps have client-side IDs for React keys, branches are plain arrays. You convert only at the edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Render the workflow definition
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;buildWorkflowDefinition(state)&lt;/code&gt; in &lt;code&gt;definition.ts&lt;/code&gt; is a pure function with no I/O. The page runs it through &lt;code&gt;useMemo&lt;/code&gt; on every change, which is why the JSON panel updates as you type. For our example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Route invoice emails"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Post invoice emails to #finance, auto-reply to everything else"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"triggers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"trigger_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"New Email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"googleMail/v1/newEmail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"condition_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"condition/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"rawExpression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"conditions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${trigger_1.subject}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONTAINS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"caseTrue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Send Channel Message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack/v1/sendChannelMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C07FINANCE1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"New invoice from ${trigger_1.from}: ${trigger_1.subject}"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"caseFalse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"googleMail_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Send Email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"googleMail/v1/sendEmail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"${trigger_1.from}"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Re: ${trigger_1.subject}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"bodyType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TEXT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Thanks, we got your message and will reply within one business day."&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole contract. Read it once and you can generate it from any UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;triggers&lt;/code&gt; and &lt;code&gt;tasks&lt;/code&gt; are arrays of nodes with a unique &lt;code&gt;name&lt;/code&gt;, a &lt;code&gt;label&lt;/code&gt;, a &lt;code&gt;type&lt;/code&gt; and &lt;code&gt;parameters&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type&lt;/code&gt; is &lt;code&gt;component/version/operation&lt;/code&gt;. Flow controls drop the operation: &lt;code&gt;condition/v1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Flow controls nest. &lt;code&gt;caseTrue&lt;/code&gt; and &lt;code&gt;caseFalse&lt;/code&gt; are task arrays, so the JSON mirrors your UI's branches. &lt;code&gt;conditions&lt;/code&gt; is a list of OR groups, each a list of AND comparisons.&lt;/li&gt;
&lt;li&gt;Parameters are typed. &lt;code&gt;to&lt;/code&gt; is an array because the field was &lt;code&gt;stringArray&lt;/code&gt;. Blank optional fields are omitted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gotcha: node names must be stable
&lt;/h3&gt;

&lt;p&gt;Nodes get a per-component counter: &lt;code&gt;slack_1&lt;/code&gt;, &lt;code&gt;googleMail_1&lt;/code&gt;, &lt;code&gt;condition_1&lt;/code&gt;. The condition is named last, after its branch actions, because the code builds branches first.&lt;/p&gt;

&lt;p&gt;That order matters. Connection binding (step 5) uses node names from a second function, &lt;code&gt;collectConnectionBindings&lt;/code&gt;, which walks state in the same order. If the two ever disagree, a connection lands on the wrong node. In production, assign the node name when the step is created and store it in state.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Let users pick their connections
&lt;/h2&gt;

&lt;p&gt;Each card has a Connection dropdown, filled by one call per component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GET /api/embedded/v1/components/{componentName}/connections&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchComponentConnections&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;googleMail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// [{id, name}, ...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user's JWT scopes the call, so each user only sees their own accounts.&lt;/p&gt;

&lt;p&gt;Connections are &lt;strong&gt;not&lt;/strong&gt; part of the definition. The definition says what to do. Which account to do it with is stored per node. That separation is what lets one definition run against thousands of users' accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Save and bind
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflowUuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createBuilderWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildWorkflowDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binding&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;collectConnectionBindings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;bindWorkflowNodeConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// e.g. "googleMail_1"&lt;/span&gt;
    &lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;componentName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "googleMail"&lt;/span&gt;
    &lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connectionId&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;failedBindings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both helpers are thin wrappers around the embedded API. Every request carries the user's JWT in &lt;code&gt;Authorization&lt;/code&gt; and the target environment in &lt;code&gt;X-Environment&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;createBuilderWorkflow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /api/embedded/v1/automation/workflows&lt;/code&gt; with &lt;code&gt;{definition}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Creates the workflow in the user's project, returns its UUID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bindWorkflowNodeConnection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PUT /api/embedded/v1/automation/workflows/{uuid}/workflow-nodes/{nodeName}/connection/{connectionKey}&lt;/code&gt; with &lt;code&gt;{connectionId}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Attaches one connection to one node&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Each user gets a project automatically.&lt;/strong&gt; ByteChef creates it the first time it's needed. Your app never manages projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;definition&lt;/code&gt; is a string.&lt;/strong&gt; Stringified JSON, the same format ByteChef's editor stores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The connection key is the component name&lt;/strong&gt; for single-connection components like Gmail and Slack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binding is best-effort.&lt;/strong&gt; The workflow is already saved, so report which nodes failed instead of failing the whole save.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb3n1voemts504iqxq4bk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb3n1voemts504iqxq4bk.png" alt="Custom Workflow Builder showing Workflow saved successfully with a View workflow link" width="560" height="110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What ByteChef sees
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;View workflow&lt;/strong&gt; and ByteChef's own editor opens your JSON as a regular graph: Gmail trigger, condition with TRUE and FALSE branches, Slack and Gmail actions, with your node names.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facb5bg6bb52usuo2o0hg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facb5bg6bb52usuo2o0hg.png" alt="ByteChef embedded editor showing the saved workflow with the condition, TRUE and FALSE branches, marked V1 DRAFT" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's also your safety net. In the demo, the Slack node is left without a connection on purpose. ByteChef flags it with a required-connection warning and a Create Connection button. Same for Gmail's New Email trigger, which needs a Pub/Sub Topic Name the catalog never asks for. ByteChef doesn't guess. It marks it required. Your UI can be as forgiving as you like, and ByteChef still validates the result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfqr2rfr6a8z1wahcisa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfqr2rfr6a8z1wahcisa.png" alt="Slack node in ByteChef editor with a required connection, a Create Connection button and one issue flagged" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  From draft to running
&lt;/h2&gt;

&lt;p&gt;Saving creates a &lt;strong&gt;DRAFT&lt;/strong&gt;. Two more calls make it run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8lj6elt2hii7avmxzpa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8lj6elt2hii7avmxzpa.png" alt="Workflows list with Route invoice emails marked DRAFT and its enable toggle disabled" width="800" height="287"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Freeze the current definition as a version (V1, V2, ...)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/embedded/v1/automation/workflows/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/publish`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;First version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Turn it on (DELETE to turn it off)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/embedded/v1/automation/workflows/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/enable`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A custom builder can fold both into one &lt;strong&gt;Activate&lt;/strong&gt; button. Once enabled, new mail starts runs on the user's bound connections.&lt;/p&gt;

&lt;p&gt;The rest of the lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /automation/workflows/{uuid}&lt;/code&gt; and &lt;code&gt;PUT /automation/workflows/{uuid}&lt;/code&gt; for editing. Re-bind connections after structural edits.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DELETE /automation/workflows/{uuid}&lt;/code&gt; to remove.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /automation/workflows&lt;/code&gt; to list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every endpoint also has a server-to-server form under &lt;code&gt;/{externalUserId}/automation/workflows/...&lt;/code&gt;, authenticated by your backend. Use it to provision default automations when a customer signs up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you ship it
&lt;/h2&gt;

&lt;p&gt;The sample is a teaching tool. Close these gaps first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validate before saving&lt;/strong&gt;, including required fields the catalog skips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store node names on steps&lt;/strong&gt; so bindings and edits stay stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offer data pills, not expressions.&lt;/strong&gt; Generate &lt;code&gt;${trigger_1.subject}&lt;/code&gt; from a field picker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle missing connections in your UI&lt;/strong&gt; by opening the ConnectDialog right there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grow the catalog on purpose.&lt;/strong&gt; Start with the few actions your users really need.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Who owns what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You own&lt;/strong&gt; the authoring experience: catalog, forms, branching UI, connection picker, wording, and the state-to-JSON function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ByteChef owns&lt;/strong&gt; everything after the JSON: validation, storage, versioning, per-user projects, connection storage and OAuth refresh, trigger registration, and execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even at maximum control, you're not building an iPaaS. You're building a front end. The moment you save, it's a real ByteChef workflow, indistinguishable from one built in ByteChef's editor, copied from a template or generated from a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Run the embedded sample app, connect Gmail under Integrations, then open &lt;strong&gt;Automations → New from Custom Workflow Builder&lt;/strong&gt;. Add a step or two, watch the JSON update, save, and click &lt;strong&gt;View workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is part 7 of the ByteChef Embedded series. It was originally published on the &lt;a href="https://blog.bytechef.io/blogs/embedded-custom-builder" rel="noopener noreferrer"&gt;ByteChef blog&lt;/a&gt;. ByteChef is open source (Apache 2.0) on &lt;a href="https://github.com/bytechefhq/bytechef" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>bytechef</category>
      <category>agents</category>
    </item>
    <item>
      <title>ByteChef Embedded, Part 6: The MCP Chat</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Tue, 06 Oct 2026 20:31:48 +0000</pubDate>
      <link>https://dev.to/bytechef/bytechef-embedded-part-6-the-mcp-chat-1c2k</link>
      <guid>https://dev.to/bytechef/bytechef-embedded-part-6-the-mcp-chat-1c2k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; In &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;part three&lt;/a&gt;, your own route fetched the user's tools from ByteChef and adapted each one to the AI SDK. The &lt;strong&gt;MCP Chat&lt;/strong&gt; lets a standard do that work: its backend route (&lt;code&gt;/api/chat-mcp&lt;/code&gt;) opens an MCP client to ByteChef's embedded MCP server over streamable HTTP, calls &lt;code&gt;mcpClient.tools()&lt;/code&gt; to &lt;strong&gt;discover the connected user's tools&lt;/strong&gt;, and hands them straight to the model. It's the same Model Context Protocol that Claude and Cursor speak, consumed inside &lt;em&gt;your own&lt;/em&gt; chat. This is &lt;strong&gt;part six&lt;/strong&gt; of the series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;ComponentKit Chat&lt;/a&gt; gave an assistant real tools, but your route had to know ByteChef's tools API: fetch the list, parse each schema, wrap it with &lt;code&gt;tool()&lt;/code&gt;, and POST every call back for execution. The &lt;strong&gt;MCP Chat&lt;/strong&gt; gets the same kind of toolbox through a &lt;strong&gt;protocol&lt;/strong&gt; instead, so discovery and execution are standardized rather than hand-written per app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Up the MCP Server in ByteChef
&lt;/h2&gt;

&lt;p&gt;Before the chat can discover anything, ByteChef needs an MCP server that says &lt;em&gt;which&lt;/em&gt; tools to expose. You build it once, as the vendor, and every connected user gets their own scoped view of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Publish an Integration First
&lt;/h3&gt;

&lt;p&gt;An embedded MCP server only offers components that one of your &lt;strong&gt;published integrations&lt;/strong&gt; uses. If you followed &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;part one&lt;/a&gt;, you already have one. Here it's a Gmail integration, and that's what makes Gmail show up in the steps below.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create the Server
&lt;/h3&gt;

&lt;p&gt;Open &lt;strong&gt;Embedded → MCP Servers&lt;/strong&gt; and click &lt;strong&gt;New MCP Server&lt;/strong&gt;. The only thing it asks for is a name. Pick one that describes what the assistant is for, since you can run several servers with different toolboxes side by side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07dy3vcdeotqv6hieify.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07dy3vcdeotqv6hieify.png" alt="Create the Server" width="560" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Add Components and Pick Their Tools
&lt;/h3&gt;

&lt;p&gt;Expand the new server and, on its &lt;strong&gt;Component Tools&lt;/strong&gt; tab, click &lt;strong&gt;Add Component&lt;/strong&gt;. The picker lists the components behind your published integrations, with a count of the tools each one offers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwh28rpxygpk2yst743y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwh28rpxygpk2yst743y.png" alt="Add Components and Pick Their Tools" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose one and you get its full list of actions. Tick only the ones you want a model to have. Gmail has eleven, including &lt;strong&gt;Delete Email&lt;/strong&gt;, so this is where you decide that the assistant can search and read mail without being able to delete it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fofgs11q0hlmchcr007ui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fofgs11q0hlmchcr007ui.png" alt="Choose one and you get its full list of actions" width="800" height="705"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Save, and the component appears on the server with the tools you picked. The gear icon (&lt;strong&gt;Configure&lt;/strong&gt;) next to each tool lets you rename it, rewrite the description the model reads, and pin any input to a fixed value. Inputs you leave alone stay &lt;strong&gt;Automatically defined by the model&lt;/strong&gt;. Pinning &lt;code&gt;Max Results&lt;/code&gt; on Search Email, for example, keeps the model from pulling a whole mailbox in one call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7bs5mmipr7detbf5z1r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7bs5mmipr7detbf5z1r.png" alt="Save, and the component appears on the server with the tools you picked" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Add Workflows as Tools (Optional)
&lt;/h3&gt;

&lt;p&gt;A single action isn't always enough. On the &lt;strong&gt;Workflow Tools&lt;/strong&gt; tab, &lt;strong&gt;Add Workflows&lt;/strong&gt; lets you expose a whole integration workflow as one tool. Pick an integration instance configuration and tick its workflows. Only workflows that start with the &lt;strong&gt;Workflow › New Workflow Call&lt;/strong&gt; trigger qualify, because that trigger defines the input schema the model fills in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgeyy35tqjau3rt03wx2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgeyy35tqjau3rt03wx2.png" alt="A single action isn't always enough" width="552" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here a &lt;code&gt;Send Email&lt;/code&gt; workflow from the Gmail integration becomes a tool next to the raw Gmail actions. A workflow tool can enforce your rules (a fixed sender, a template, a log step) that a bare action can't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftr87pwhccg5zkxbghir1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftr87pwhccg5zkxbghir1.png" alt="Here a  raw `Send Email` endraw  workflow from the Gmail integration becomes a tool next to the raw Gmail actions" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Enable It and Copy the Server URL
&lt;/h3&gt;

&lt;p&gt;Switch the server's toggle on, then open the &lt;strong&gt;Connect&lt;/strong&gt; tab. The &lt;strong&gt;Server URL&lt;/strong&gt; has the form &lt;code&gt;https://&amp;lt;your-bytechef&amp;gt;/api/embedded/{secretKey}/mcp&lt;/code&gt;. It's marked &lt;strong&gt;Sensitive&lt;/strong&gt; because the secret key in the path identifies your server. The copy icon copies it, and the refresh icon rotates the secret if it ever leaks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp4tr23vtrlbpyy72pme.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp4tr23vtrlbpyy72pme.png" alt="Switch the server's toggle on, then open the **Connect** tab" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That URL is the only server-side setting the sample app needs. Put it in &lt;code&gt;front-end/.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NEXT_PUBLIC_BYTECHEF_MCP_SERVER_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-bytechef/api/embedded/&lt;span class="o"&gt;{&lt;/span&gt;secretKey&lt;span class="o"&gt;}&lt;/span&gt;/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL picks the server; the connected-user JWT your route sends picks &lt;em&gt;whose&lt;/em&gt; tools and connections to use. Every user shares one URL and still gets their own toolbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Per-User Control
&lt;/h3&gt;

&lt;p&gt;Once a connected user has connected an integration whose component is on the server, the server shows up in their record. Open &lt;strong&gt;Embedded → Connected Users&lt;/strong&gt;, click the user, and switch to the &lt;strong&gt;MCP Servers&lt;/strong&gt; tab. Each server lists that user's tools in two groups, &lt;strong&gt;Component Tools&lt;/strong&gt; and &lt;strong&gt;Workflow Tools&lt;/strong&gt;, under the integration they come from, with its connection status and version. Workflow tools like &lt;strong&gt;Send Email&lt;/strong&gt; sit there next to plain actions like Search Email. You can turn the whole server off for that one user, or switch off individual tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbeqb5iif4fa8j4d2utw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbeqb5iif4fa8j4d2utw.png" alt="Once a connected user has connected an integration whose component is on the server, the server shows up in their record" width="768" height="790"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your users get the same control. When they open the &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;ConnectDialog&lt;/a&gt; for a connected integration, its &lt;strong&gt;Tools&lt;/strong&gt; tab lists the integration's tools with a toggle for each. Workflow tools like &lt;strong&gt;Send Email&lt;/strong&gt; sit next to plain actions, with any inputs the workflow asks for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0arg1ozhmp1zx3sche6s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0arg1ozhmp1zx3sche6s.png" alt="Your users get the same control" width="520" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note the default: when a user connects (or reconnects) an integration, its tools start &lt;strong&gt;switched off&lt;/strong&gt;. Until the user turns them on, the assistant can't use them. That's a sensible opt-in for anything that acts in someone's mailbox, but it means your UI should point users at this tab after they connect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools by Discovery
&lt;/h2&gt;

&lt;p&gt;The chat page is the same assistant-ui thread as part three. The difference is entirely in the backend route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMCPClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StreamableHTTPClientTransport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/client/streamableHttp.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwtToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mcpClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createMCPClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamableHTTPClientTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BYTECHEF_MCP_SERVER_URL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;requestInit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Environment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BYTECHEF_ENVIRONMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mcpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// discover the connected user's tools&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;frontendTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientTools&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;onFinish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mcpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of enumerating and wrapping actions yourself, you open an MCP connection to ByteChef's embedded MCP server and ask it for the &lt;strong&gt;tools available to this connected user&lt;/strong&gt;. The same JWT from &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;part one&lt;/a&gt; rides along in the &lt;code&gt;Authorization&lt;/code&gt; header, so the user's integrations (the ones &lt;em&gt;you&lt;/em&gt; configured for them as a vendor) arrive as ready-to-call tools, already scoped to their connections. There's no &lt;code&gt;execute&lt;/code&gt; function to write: when the model calls a tool, the MCP client sends the call back to the server for you.&lt;/p&gt;

&lt;p&gt;A few details worth noticing in the route:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stopWhen: stepCountIs(8)&lt;/code&gt;&lt;/strong&gt; lets the agent loop continue past tool calls, so the model can read a tool's result and act again (or answer) instead of stopping after one step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;frontendTools(...)&lt;/code&gt;&lt;/strong&gt; merges in any tools the assistant-ui page registers on the client, so server tools from MCP and browser-side tools live in one toolbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mcpClient.close()&lt;/code&gt;&lt;/strong&gt; runs in &lt;code&gt;onFinish&lt;/code&gt;, so each request opens and closes its own MCP session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add a tool to the MCP server on the ByteChef side, and it shows up in the chat on the next request. No client change.&lt;/p&gt;

&lt;p&gt;Here's one turn in the sample app. The model finds &lt;code&gt;GOOGLEMAIL_SEARCH_EMAIL&lt;/code&gt; among the discovered tools, fills in the &lt;code&gt;subject&lt;/code&gt;, and answers from the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuzkavoohljeajo38mgw8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuzkavoohljeajo38mgw8.png" alt="Here's one turn in the sample app" width="740" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tool names follow a &lt;code&gt;COMPONENT_ACTION&lt;/code&gt; pattern, so the Gmail &lt;strong&gt;Search Email&lt;/strong&gt; action becomes &lt;code&gt;GOOGLEMAIL_SEARCH_EMAIL&lt;/code&gt;. A workflow tool is named after its workflow, so the &lt;code&gt;Send Email&lt;/code&gt; workflow from step 4 arrives as &lt;code&gt;Send_Email&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authorization on Demand
&lt;/h2&gt;

&lt;p&gt;The user doesn't have to connect every app up front. If the model calls a tool whose integration isn't connected yet, the embedded MCP server doesn't fail the call. It returns a result that tells the model what to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"connection_required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The googleMail integration is not connected for this user. To connect, visit: https://your-bytechef/connect.html?token=... . Instruct the user to visit this link to connect their account. Present it as a markdown link labelled &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Connect googleMail&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; instead of printing the raw URL."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"setupUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-bytechef/connect.html?token=..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message is written for the model, not the user: it says what went wrong, where to send the user, and even how to format the link. Here's the same Gmail search from above, asked by a user who hasn't connected Gmail yet:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6r68xflrzk1ucrehsh2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6r68xflrzk1ucrehsh2.png" alt="The message is written for the model, not the user" width="700" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are two buttons, and they come from different places. The model wrote &lt;strong&gt;Connect googleMail&lt;/strong&gt; into its answer because the message asked it to. &lt;strong&gt;Connect account&lt;/strong&gt; under the tool result comes from the sample app's chat UI, which spots &lt;code&gt;error: "connection_required"&lt;/code&gt; and renders &lt;code&gt;setupUrl&lt;/code&gt; as a button, so the link is there even if a model ignores the instruction.&lt;/p&gt;

&lt;p&gt;The model passes the link on in the chat. The link opens ByteChef's hosted connect page, which carries a short-lived token (valid for 10 minutes) scoped to that user and integration. The page opens the same &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;ConnectDialog&lt;/a&gt; from part one: an OAuth2 popup for OAuth2 apps, or a form for API keys and other credential types.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph43bw7psyl86rrxh5rr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph43bw7psyl86rrxh5rr.png" alt="The model passes the link on in the chat" width="520" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the user connects, nothing else is needed. The server checks for a connection on every tool call, so when the model tries the tool again, it runs on the user's new connection.&lt;/p&gt;

&lt;p&gt;That turns onboarding into part of the conversation: the user asks for something, the assistant finds it needs Slack, hands over a link, and carries on once Slack is connected. Connections only get created for the apps people actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Protocol Matters
&lt;/h2&gt;

&lt;p&gt;Doing this over MCP rather than through a custom adapter buys you three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One definition, many clients.&lt;/strong&gt; The tools your MCP Chat uses are the &lt;em&gt;same ones&lt;/em&gt; an external Claude or Cursor gets from the embedded MCP server. You define an integration's tool surface once, and every AI (yours and your customers') consumes it the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Central governance.&lt;/strong&gt; Which tools exist, their input schemas, and the per-connected-user identity are all resolved server-side. The chat just discovers and calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future-proofing.&lt;/strong&gt; MCP is the emerging standard for giving models tools. Building on it means your "AI that acts" feature speaks the same language as the rest of the industry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same outcome as the &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;ComponentKit Chat&lt;/a&gt; (an agent that operates the user's apps), but the tools flow through an open, discoverable protocol instead of bespoke wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six Parts In
&lt;/h2&gt;

&lt;p&gt;The pieces so far compose into one story about what "embedded automation" takes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;Connect&lt;/a&gt;&lt;/strong&gt; your users' apps (the JWT and ConnectDialog everything rests on).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit" rel="noopener noreferrer"&gt;Act&lt;/a&gt;&lt;/strong&gt; on those connected apps directly with the ComponentKit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;Chat&lt;/a&gt;&lt;/strong&gt; with an AI assistant that uses the ComponentKit's tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.bytechef.io/blogs/embedded-app-events" rel="noopener noreferrer"&gt;Trigger&lt;/a&gt;&lt;/strong&gt; their workflows from your product's events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.bytechef.io/blogs/embedded-request" rel="noopener noreferrer"&gt;Call&lt;/a&gt;&lt;/strong&gt; a workflow like a synchronous API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent over MCP&lt;/strong&gt;: the same tools, discovered through an open protocol (this part).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every one of them was a component or a couple of API calls, all scoped by a single JWT and all riding the same &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;connections&lt;/a&gt;. That's the promise of embedded: the integration &lt;em&gt;surface&lt;/em&gt; is yours (your UI, your brand, your product) and the integration &lt;em&gt;machinery&lt;/em&gt; is ByteChef's.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Following along? Create an MCP server under **Embedded → MCP Servers&lt;/em&gt;&lt;em&gt;, paste its URL into &lt;code&gt;NEXT_PUBLIC_BYTECHEF_MCP_SERVER_URL&lt;/code&gt;, set &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, connect the integration in the &lt;a href="https://github.com/bytechefhq/bytechef-embedded-sample-app" rel="noopener noreferrer"&gt;embedded sample app&lt;/a&gt;, and open MCP Chat. Your assistant now finds its tools on its own.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>bytechef</category>
      <category>mcp</category>
    </item>
    <item>
      <title>ByteChef Embedded, Part 5: The Request Playground</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Wed, 30 Sep 2026 15:28:59 +0000</pubDate>
      <link>https://dev.to/bytechef/bytechef-embedded-part-5-the-request-playground-16bd</link>
      <guid>https://dev.to/bytechef/bytechef-embedded-part-5-the-request-playground-16bd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; In &lt;a href="https://blog.bytechef.io/blogs/embedded-app-events" rel="noopener noreferrer"&gt;part four&lt;/a&gt;, App Events triggered &lt;em&gt;whoever's listening&lt;/em&gt;. Sometimes you instead want to invoke &lt;strong&gt;one specific workflow&lt;/strong&gt; and get its output back. The &lt;strong&gt;Request Playground&lt;/strong&gt; does exactly that: &lt;code&gt;POST /api/embedded/v1/workflows/{workflowUuid}&lt;/code&gt; with a JSON body runs that workflow for the current &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;connected user&lt;/a&gt;, passing the body as trigger input and returning the result. It turns any automation into a &lt;strong&gt;synchronous API endpoint&lt;/strong&gt; your app can call like a function. This is &lt;strong&gt;part five&lt;/strong&gt; of the series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;App Events are fire-and-fan-out: you emit, everyone listening runs, you don't wait for a result. But plenty of use cases are request/response: "run &lt;em&gt;this&lt;/em&gt; enrichment workflow on &lt;em&gt;this&lt;/em&gt; record and give me back the answer." For that you don't want an event; you want to &lt;strong&gt;call a workflow directly&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Up the Request Trigger
&lt;/h2&gt;

&lt;p&gt;A workflow becomes callable once it starts with the &lt;strong&gt;Request&lt;/strong&gt; trigger. In the workflow editor, add a trigger (or right-click an existing one and choose &lt;strong&gt;Replace&lt;/strong&gt;), search for &lt;strong&gt;Request&lt;/strong&gt;, and pick one of its two triggers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto Respond with HTTP 200 Status&lt;/strong&gt; - accepts the call, replies &lt;code&gt;200&lt;/code&gt; right away, and runs the workflow in the background. Fire-and-forget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Await Workflow and Respond&lt;/strong&gt; - holds the request open until the workflow finishes, then returns its result. This is the one that makes a workflow behave like a function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F40v0jbeliinkp96xlie0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F40v0jbeliinkp96xlie0.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Await Workflow and Respond&lt;/strong&gt; has one property, &lt;strong&gt;Timeout (ms)&lt;/strong&gt;: how long the caller waits before the request times out, with a maximum of five minutes. Set it lower if your app shouldn't block that long.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvijp7hoyvdwsd3l3s71q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvijp7hoyvdwsd3l3s71q.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add the steps that do the work below the trigger. The request body is available to them as the trigger's output. Then &lt;strong&gt;Publish&lt;/strong&gt; the workflow; once a connected user has the integration enabled, it's ready to call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the Workflow UUID
&lt;/h2&gt;

&lt;p&gt;Every integration workflow has a stable &lt;code&gt;workflowUuid&lt;/code&gt;, and that's what the call needs. You get it from the Embedded API with the same connected-user JWT, in two calls. &lt;code&gt;GET /api/embedded/v1/integrations&lt;/code&gt; lists the user's active integrations. &lt;code&gt;GET /api/embedded/v1/integrations/{id}&lt;/code&gt; returns one of them with its &lt;code&gt;workflows&lt;/code&gt;, and each workflow has a &lt;code&gt;label&lt;/code&gt; and a &lt;code&gt;workflowUuid&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchWorkflowUuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;integrationName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;workflowLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// GET /api/embedded/v1/integrations   (bearer JWT)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;integrations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/embedded/v1/integrations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;integrationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;integrations&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;integrationName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;integrationId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`No active integration named "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;integrationName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// GET /api/embedded/v1/integrations/{id}   (bearer JWT)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;integration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/embedded/v1/integrations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;integrationId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflowUuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workflows&lt;/span&gt;
    &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;workflowLabel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Integration "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;integrationName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" has no workflow labeled "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowLabel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflowUuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWorkflowUuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HubSpot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enrich contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list only includes integrations the connected user has active, so enable the integration for that user first. The UUID stays the same across published versions, so you can look it up once and keep it in your app's config.&lt;/p&gt;

&lt;p&gt;In the sample app, the Request Playground's &lt;strong&gt;Find the Workflow UUID&lt;/strong&gt; card makes the same two calls. Pick an integration, then click &lt;strong&gt;Use&lt;/strong&gt; next to a workflow to fill in its UUID.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Workflow, Callable Like a Function
&lt;/h2&gt;

&lt;p&gt;The playground takes that workflow UUID and a body, and invokes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST /api/embedded/v1/workflows/{workflowUuid}   (bearer JWT)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/request/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workflowUuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello from the Request Playground&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ByteChef runs that specific workflow for the current connected user, feeding your body in as the trigger's input, and returns the workflow's output in the response. From your app's perspective, a whole multi-step automation (connectors, branches, the works) behaves like a single API call: request in, result out.&lt;/p&gt;

&lt;p&gt;This is the pattern behind &lt;strong&gt;request-triggered workflows&lt;/strong&gt;: your customer builds an automation with a request-style trigger, and you (or they) invoke it on demand. The workflow becomes a named, versioned, connection-backed function, one your app can call synchronously wherever it needs the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Fits Among the Triggers
&lt;/h2&gt;

&lt;p&gt;The sample app now shows all three ways work &lt;em&gt;enters&lt;/em&gt; a workflow, and they're complementary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://blog.bytechef.io/blogs/guide-to-flow-controls" rel="noopener noreferrer"&gt;Native triggers&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a schedule fires, a webhook lands, a poll finds new data&lt;/td&gt;
&lt;td&gt;the automation reacts to the outside world&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;a href="https://blog.bytechef.io/blogs/embedded-app-events" rel="noopener noreferrer"&gt;App Events&lt;/a&gt;&lt;/strong&gt; (part 4)&lt;/td&gt;
&lt;td&gt;your product emits an event, fanned to all listeners&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;your&lt;/em&gt; product's events drive automations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Request&lt;/strong&gt; (this part)&lt;/td&gt;
&lt;td&gt;your app calls one workflow by UUID and awaits its result&lt;/td&gt;
&lt;td&gt;you need a specific automation's output, now&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Together they mean a workflow can be scheduled, event-driven, &lt;em&gt;or&lt;/em&gt; called like an API: the same automation reachable three ways, all scoped by the same JWT.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Didn't Build
&lt;/h2&gt;

&lt;p&gt;For one POST:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous invocation&lt;/strong&gt; of a specific workflow by id, per connected user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input delivery and output collection&lt;/strong&gt; through the &lt;a href="https://blog.bytechef.io/blogs/bytechef-architecture" rel="noopener noreferrer"&gt;execution engine&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A stable &lt;strong&gt;"automation as an endpoint"&lt;/strong&gt; contract your app can depend on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No queue to manage, no polling for results. Call it, await it, use the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Five parts in, the pieces fit together: the &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;ConnectDialog&lt;/a&gt; links your customer's accounts, the &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit" rel="noopener noreferrer"&gt;ComponentKit&lt;/a&gt; and its &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;AI Chat&lt;/a&gt; act on those apps, &lt;a href="https://blog.bytechef.io/blogs/embedded-app-events" rel="noopener noreferrer"&gt;App Events&lt;/a&gt; let your product start workflows, and the Request Playground turns a workflow into an endpoint you call and await. Next up is the sample app's &lt;strong&gt;MCP Chat&lt;/strong&gt;, which exposes the same tools through the Model Context Protocol, so clients like Claude and Cursor can use them too.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Following along? POST a payload to a workflow UUID and read the response - you've turned an automation into a callable function.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>ByteChef Embedded, Part 4: The App Event Playground</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:25:58 +0000</pubDate>
      <link>https://dev.to/bytechef/bytechef-embedded-part-4-the-app-event-playground-29p7</link>
      <guid>https://dev.to/bytechef/bytechef-embedded-part-4-the-app-event-playground-29p7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; In &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit" rel="noopener noreferrer"&gt;part two&lt;/a&gt; and &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;part three&lt;/a&gt; your app called &lt;em&gt;into&lt;/em&gt; connected apps, one action at a time or through an AI assistant. &lt;strong&gt;App Events&lt;/strong&gt; are the opposite direction: your product emits an event and ByteChef triggers your customers' workflows from it. &lt;code&gt;POST /api/embedded/v1/app-events&lt;/code&gt; with a free-form JSON body fans that payload out to &lt;strong&gt;every enabled workflow's app-event trigger&lt;/strong&gt; for the current &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;connected user&lt;/a&gt;. It's how "something happened in &lt;em&gt;my&lt;/em&gt; SaaS" becomes "run the automations my customer built around it." This is &lt;strong&gt;part four&lt;/strong&gt; of the series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Half of embedded automation is your customers reacting to &lt;em&gt;external&lt;/em&gt; apps (a new row in their Google Sheet). The other half, the half that makes your product the &lt;em&gt;center&lt;/em&gt; of their automations, is reacting to events in &lt;strong&gt;your&lt;/strong&gt; app. When a user upgrades their plan, closes a deal, or gets a new signup &lt;em&gt;in your product&lt;/em&gt;, they'll want to automate what happens next. &lt;strong&gt;App Events&lt;/strong&gt; are the inbound trigger that makes your product a first-class event source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emit an Event, Trigger Their Workflows
&lt;/h2&gt;

&lt;p&gt;The playground is a single POST with a free-form body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST /api/embedded/v1/app-events   (bearer JWT)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/app-events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the ByteChef side, that call iterates &lt;strong&gt;every enabled integration&lt;/strong&gt; the current user has, finds each workflow with an &lt;strong&gt;app-event trigger&lt;/strong&gt;, and forwards your body as the event payload. One emit, fanned out to all the automations listening for it. The body shape is yours. It should match the JSON schema you defined for the event, so the workflows can map fields off it.&lt;/p&gt;

&lt;p&gt;Put that in your product's backend, and every meaningful thing that happens becomes a potential automation trigger for your customers: &lt;code&gt;subscription.upgraded&lt;/code&gt;, &lt;code&gt;lead.created&lt;/code&gt;, &lt;code&gt;ticket.resolved&lt;/code&gt;. You emit once; ByteChef routes it to whoever built a workflow around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  See Every Run
&lt;/h2&gt;

&lt;p&gt;Each workflow an event starts is a normal workflow execution, so it shows up on the &lt;strong&gt;Workflow Executions&lt;/strong&gt; page under Embedded in ByteChef. You can filter by integration, workflow, status, and date range, and open any run to see the trigger payload your product sent and what each step did with it. When a customer asks "why didn't my automation fire?", that's where you look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Closes the Loop
&lt;/h2&gt;

&lt;p&gt;Together, the &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit" rel="noopener noreferrer"&gt;ComponentKit&lt;/a&gt; and App Events make your product a full node in your customers' automation graph. Data flows &lt;em&gt;both&lt;/em&gt; ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App Events (in):&lt;/strong&gt; your product's events &lt;em&gt;start&lt;/em&gt; their workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit" rel="noopener noreferrer"&gt;ComponentKit&lt;/a&gt; (out):&lt;/strong&gt; your app, or an &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;AI assistant&lt;/a&gt; inside it, &lt;em&gt;acts on&lt;/em&gt; connected services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That bidirectionality is what turns "we have some integrations" into "our product is the hub." An event in &lt;em&gt;your&lt;/em&gt; product can trigger a workflow that updates your customer's CRM, posts to their Slack, or calls back into your own API. ByteChef is the router in the middle, and App Events are your product's on-ramp to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Didn't Build
&lt;/h2&gt;

&lt;p&gt;For one POST:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;event ingestion&lt;/strong&gt; endpoint that authenticates and scopes to a connected user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out routing&lt;/strong&gt; to every matching workflow trigger across all the user's integrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload delivery&lt;/strong&gt; into the execution engine as trigger input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution history&lt;/strong&gt; for every run the event started, on the Workflow Executions page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No event bus, no subscription registry, no per-workflow dispatch. You emit; ByteChef routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;App Events start workflows implicitly: whoever's listening runs. Sometimes you want to invoke a &lt;em&gt;specific&lt;/em&gt; workflow directly and get its result back. That's the next part, the &lt;strong&gt;Request Playground&lt;/strong&gt;, where a workflow behaves like a synchronous API endpoint.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Following along? POST an event body to &lt;code&gt;/app-events&lt;/code&gt; from your product's backend - every workflow your customers built around that event fires.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>bytechef</category>
      <category>agents</category>
    </item>
    <item>
      <title>ByteChef Embedded, Part 2: The ComponentKit Playground</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Thu, 17 Sep 2026 13:08:17 +0000</pubDate>
      <link>https://dev.to/bytechef/bytechef-embedded-part-2-the-componentkit-playground-5egn</link>
      <guid>https://dev.to/bytechef/bytechef-embedded-part-2-the-componentkit-playground-5egn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Not everything needs a workflow. The &lt;strong&gt;ComponentKit&lt;/strong&gt; lets your app call &lt;strong&gt;any connector action directly&lt;/strong&gt; on a &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;connected user's&lt;/a&gt; account: &lt;code&gt;POST /api/embedded/v1/{externalUserId}/components/{component}/versions/{version}/actions/{action}&lt;/code&gt; with an &lt;code&gt;{ input }&lt;/code&gt; body runs, say, &lt;code&gt;slack&lt;/code&gt; / &lt;code&gt;sendChannelMessage&lt;/code&gt; or &lt;code&gt;openai&lt;/code&gt; / &lt;code&gt;ask&lt;/code&gt; with the user's stored credentials and returns the result. The &lt;a href="https://github.com/bytechefhq/bytechef-embedded-sample-app" rel="noopener noreferrer"&gt;embedded sample app&lt;/a&gt;'s &lt;strong&gt;ComponentKit Playground&lt;/strong&gt; is a form over that one endpoint. This is &lt;strong&gt;part two&lt;/strong&gt; of the series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog" rel="noopener noreferrer"&gt;part one&lt;/a&gt; your users connected their apps. An embedded platform is more than a workflow engine, though. It is also a pre-authenticated API client for 280+ services, and sometimes you want that directly. Your app needs to post one Slack message, enrich one contact, or run one OpenAI completion, &lt;em&gt;as your customer&lt;/em&gt;, without authoring or running a workflow for it. That's the &lt;strong&gt;ComponentKit&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Playground Page
&lt;/h2&gt;

&lt;p&gt;The page is a form with four fields and a result panel. Pick a component, a version, an action, and a JSON input, and it posts them to the app's own API route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ComponentKitPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;componentName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setComponentName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;componentVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setComponentVersion&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;actionName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setActionName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ask&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;inputJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setInputJson&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FormEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/component-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;actionName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;componentName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;componentVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// ...a Card with the four inputs, and a Card that renders `result` in a &amp;lt;pre&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The defaults are &lt;code&gt;openai&lt;/code&gt; / version &lt;code&gt;1&lt;/code&gt; / &lt;code&gt;ask&lt;/code&gt;, and the input textarea is pre-filled with a chat request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ADVANCED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Say hello"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"responseFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TEXT"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Execute Action&lt;/strong&gt; and the response lands in the right-hand card, as JSON. Errors from ByteChef land there too, so a wrong action name or a missing required input shows you exactly what the server complained about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Route: One POST to ByteChef
&lt;/h2&gt;

&lt;p&gt;The route adds the connected user's JWT and forwards the call to the ComponentKit endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BYTECHEF_APP_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/embedded/v1/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BYTECHEF_EXTERNAL_USER_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/components/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;componentName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/versions/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;componentVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/actions/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;actionName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Environment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BYTECHEF_ENVIRONMENT&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ByteChef finds the integration this user connected for that component, takes the connection behind it, and runs the action with those credentials. The response comes back as-is. No workflow, no trigger, no execution history, just a synchronous "do this thing in their account."&lt;/p&gt;

&lt;p&gt;The shape is universal. Swap in &lt;code&gt;slack&lt;/code&gt; / &lt;code&gt;sendChannelMessage&lt;/code&gt;, &lt;code&gt;googleSheets&lt;/code&gt; / &lt;code&gt;insertRow&lt;/code&gt;, or &lt;code&gt;hubspot&lt;/code&gt; / &lt;code&gt;createContact&lt;/code&gt; and nothing else changes. The action's input schema is the same one the workflow editor uses, so if you know what a field looks like in a workflow, you know what to put in &lt;code&gt;input&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If a user has connected the same app more than once, pass the &lt;code&gt;X-Instance-Id&lt;/code&gt; header to say which integration instance you mean. Without it, ByteChef picks the user's instance for that component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;The ComponentKit turns ByteChef Embedded from "a workflow feature" into "an integration layer for your whole product." Anywhere in your app where you'd otherwise hand-roll an API client, with OAuth, token refresh, request signing, and response parsing, times 280 services, you make one call instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-product actions.&lt;/strong&gt; A "Send to Slack" button that just works, on the user's connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI tool calls.&lt;/strong&gt; Give an LLM the ability to &lt;em&gt;act&lt;/em&gt; in the user's apps, which is exactly what &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;part three&lt;/a&gt; builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend glue.&lt;/strong&gt; Server-side enrichment, sync, or notification without standing up a workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because it rides the same connection and JWT as everything else in this series, it's automatically scoped, authenticated, and refreshed. You send one POST; ByteChef is the API client you didn't write.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Didn't Build
&lt;/h2&gt;

&lt;p&gt;For one POST:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;typed action invocation&lt;/strong&gt; across 280+ connectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential resolution and refresh&lt;/strong&gt;, per connected user, per environment.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;auth handshake and request&lt;/strong&gt; for whichever provider you named.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;uniform response shape&lt;/strong&gt; instead of hundreds of bespoke API clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running It Yourself
&lt;/h2&gt;

&lt;p&gt;On top of the setup from &lt;a href="https://blog.bytechef.io/blogs/embedded-connect-dialog#running-the-sample-app-yourself" rel="noopener noreferrer"&gt;part one&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connect the integration you want to call.&lt;/strong&gt; The playground defaults to OpenAI, so connect an OpenAI integration for your demo user through the connect dialog first. The API key lives in that connection, not in the sample app's &lt;code&gt;.env.local&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try another app.&lt;/strong&gt; Connect Slack, change the form to &lt;code&gt;slack&lt;/code&gt; / &lt;code&gt;1&lt;/code&gt; / &lt;code&gt;sendChannelMessage&lt;/code&gt;, and give it a &lt;code&gt;channel&lt;/code&gt; and &lt;code&gt;text&lt;/code&gt;. Same page, same route, different account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The ComponentKit is your app calling one action at a time. Hand those same actions to a model and it can pick which one to call, fill in the input, and read the result. That's &lt;a href="https://blog.bytechef.io/blogs/embedded-component-kit-chat" rel="noopener noreferrer"&gt;&lt;strong&gt;part three: the ComponentKit Chat&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Following along? Connect an integration, open the ComponentKit Playground, and execute one action on a connected account. It's the simplest possible embedded integration.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>agents</category>
      <category>bytechef</category>
    </item>
    <item>
      <title>ByteChef Embedded, Part 1: Let Your Users Connect Their Apps</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Tue, 15 Sep 2026 09:50:21 +0000</pubDate>
      <link>https://dev.to/bytechef/bytechef-embedded-part-1-let-your-users-connect-their-apps-52bo</link>
      <guid>https://dev.to/bytechef/bytechef-embedded-part-1-let-your-users-connect-their-apps-52bo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Before a customer can automate anything in your product, they have to &lt;strong&gt;connect their own accounts&lt;/strong&gt;. &lt;a href="https://docs.bytechef.io/platform/embedded/get-started" rel="noopener noreferrer"&gt;ByteChef Embedded&lt;/a&gt; makes that a two-piece job: (1) your &lt;strong&gt;backend signs a JWT&lt;/strong&gt; identifying one of your users as a ByteChef &lt;em&gt;connected user&lt;/em&gt;, and (2) your &lt;strong&gt;frontend calls one hook&lt;/strong&gt; - &lt;code&gt;useConnectDialog&lt;/code&gt; from &lt;code&gt;@bytechef/embedded&lt;/code&gt; - which mounts a hosted &lt;strong&gt;ConnectDialog&lt;/strong&gt; that runs the whole OAuth/API-key handshake, stores and refreshes the credentials, and doubles as the &lt;strong&gt;manage&lt;/strong&gt; surface once a connection exists. This is &lt;strong&gt;part one&lt;/strong&gt; of a hands-on series that walks the &lt;a href="https://github.com/bytechefhq/bytechef-embedded-sample-app" rel="noopener noreferrer"&gt;embedded sample app&lt;/a&gt; feature by feature. We start where every embedded integration starts: connecting an app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're embedding automation &lt;em&gt;inside your own SaaS product&lt;/em&gt;, the very first wall you hit isn't workflows - it's &lt;strong&gt;connections&lt;/strong&gt;. Before your customer can "sync new leads to their CRM," they have to connect &lt;em&gt;their&lt;/em&gt; CRM, with &lt;em&gt;their&lt;/em&gt; OAuth grant, stored securely, refreshed forever. Do that yourself and you've signed up to build and maintain OAuth for Salesforce, and HubSpot, and Slack, and the next hundred apps your customers ask for. That's not a feature; it's a department.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.bytechef.io/platform/embedded/get-started" rel="noopener noreferrer"&gt;ByteChef Embedded&lt;/a&gt; exists to delete that work, and this series shows exactly how - using the real &lt;strong&gt;embedded sample app&lt;/strong&gt; (a Next.js frontend + a small Node backend) as our guide. Over the coming parts we'll build out each way your users can create automations. But all of it rests on this first step, so that's where we begin: &lt;strong&gt;letting your users connect their apps.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Auth Model: Connected Users, Not ByteChef Accounts
&lt;/h2&gt;

&lt;p&gt;Here's the crucial idea that makes embedded work: &lt;strong&gt;your end users never get a ByteChef account.&lt;/strong&gt; They're &lt;em&gt;your&lt;/em&gt; users. ByteChef knows them only as &lt;strong&gt;connected users&lt;/strong&gt;, identified by a token &lt;em&gt;your&lt;/em&gt; backend signs.&lt;/p&gt;

&lt;p&gt;The sample app's backend does exactly this - one endpoint that mints a JWT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;externalUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// your app's user id&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;externalUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;iat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BYTECHEF_PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RS256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TOKEN_EXPIRY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;keyid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BYTECHEF_KID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You hold the &lt;strong&gt;private key&lt;/strong&gt;; ByteChef verifies with the matching public key. The &lt;code&gt;sub&lt;/code&gt; claim is &lt;em&gt;your&lt;/em&gt; user id (&lt;code&gt;externalUserId&lt;/code&gt;) - so a token says, in effect, "this is my user #1234, acting as themselves." Your frontend fetches that token from your backend and never touches the key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// POSTs to your /api/token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire identity story. Every embedded call the frontend makes carries this JWT (plus an &lt;code&gt;X-ENVIRONMENT&lt;/code&gt; header), and ByteChef scopes everything - connections, workflows, executions - to that connected user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listing the Integrations You Offer
&lt;/h2&gt;

&lt;p&gt;Your users can only connect what you've published. The sample app fetches your catalog straight from the embedded API with the bearer token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BYTECHEF_APP_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/embedded/v1/integrations`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-ENVIRONMENT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BYTECHEF_ENVIRONMENT&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back comes the list of &lt;strong&gt;integrations&lt;/strong&gt; you've defined - title, icon, category, and whether this user already has an instance connected. Render them as a grid of cards, and you've got an "Integrations" page with zero bespoke per-app code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Star of Part One: &lt;code&gt;useConnectDialog&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Now the actual connecting. It's one hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useConnectDialog&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@bytechef/embedded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openDialog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;closeDialog&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useConnectDialog&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// DEVELOPMENT | STAGING | PRODUCTION&lt;/span&gt;
  &lt;span class="nx"&gt;integrationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// which integration the user clicked&lt;/span&gt;
  &lt;span class="nx"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// the connected-user token&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nf"&gt;openDialog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;         &lt;span class="c1"&gt;// mounts the hosted connect flow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;openDialog()&lt;/code&gt; and ByteChef mounts its &lt;strong&gt;ConnectDialog&lt;/strong&gt; into a portal in your page. From the user's side it's a clean, hosted modal: pick the account, get bounced through the provider's OAuth consent (or enter an API key), and land back connected. From &lt;em&gt;your&lt;/em&gt; side, the authorize URL, the callback, the code exchange and the scope handling all run inside that portal - per provider - and the tokens that come back are stored encrypted against this connected user, in this environment, then refreshed for as long as the connection lives. You never see or hold a credential.&lt;/p&gt;

&lt;p&gt;The dialog owns its own lifecycle, so its closing is something you observe rather than subscribe to: the hook hands back &lt;code&gt;openDialog&lt;/code&gt; and &lt;code&gt;closeDialog&lt;/code&gt;, and the sample app watches the &lt;code&gt;#connect-dialog-portal&lt;/code&gt; node empty out to know when to refresh its list. That's it - that's a production-grade "Connect your Salesforce" button, for any of ByteChef's 280+ connectors, without you writing a single OAuth line.&lt;/p&gt;

&lt;h2&gt;
  
  
  After the Connect: the Same Dialog Manages It
&lt;/h2&gt;

&lt;p&gt;A connection is not a one-time event. Users come back to change what is switched on, or to revoke it altogether, and that does not need a second UI: &lt;code&gt;openDialog()&lt;/code&gt; already covers both cases.&lt;/p&gt;

&lt;p&gt;When the dialog opens it looks for an existing instance, the one named by &lt;code&gt;integrationInstanceId&lt;/code&gt; if you passed it, otherwise the first this connected user already has. Find one and it opens in &lt;strong&gt;manage&lt;/strong&gt; mode instead of the auth flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;openDialog&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useConnectDialog&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;integrationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;jwtToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;integrationInstanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// optional: manage one specific connection&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there the user can toggle which of your published workflows run for their account, edit the inputs a workflow asks them for, or disconnect. Each is a call ByteChef makes against &lt;code&gt;/api/embedded/v1/integration-instances/...&lt;/code&gt; on their behalf. Disconnecting deletes the instance and closes the dialog, so the next &lt;code&gt;openDialog()&lt;/code&gt; starts the auth flow again.&lt;/p&gt;

&lt;p&gt;That fallback to the first instance is why the sample app's integration card can stay so simple: it flips its label between "Connect" and "Connected" purely on whether &lt;code&gt;integrationInstances&lt;/code&gt; is empty, and calls the same hook either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Didn't Build
&lt;/h2&gt;

&lt;p&gt;Step back and total up what the ConnectDialog absorbed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Per-provider &lt;strong&gt;OAuth flows&lt;/strong&gt; (×280), callbacks, scope handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted credential storage&lt;/strong&gt;, scoped per user and environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token refresh&lt;/strong&gt;, forever.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;manage-connection UI&lt;/strong&gt;: toggle workflows, edit their inputs, disconnect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment separation&lt;/strong&gt; (Development / Staging / Production) out of the box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your side of the contract was two small pieces: sign a JWT in your backend, call one hook in your frontend. That's the whole promise of embedded - the integration surface your customers see is yours; the integration &lt;em&gt;machinery&lt;/em&gt; is ByteChef's.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next in This Series
&lt;/h2&gt;

&lt;p&gt;Connecting an app is the foundation. Everything else is about turning that connection into automation - and the sample app's &lt;strong&gt;Automations&lt;/strong&gt; page offers five different front doors to do it, each of which gets its own part:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New from Template&lt;/strong&gt; - start from a pre-built workflow you've published.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New from Embedded Workflow Builder&lt;/strong&gt; - drop ByteChef's full visual builder into your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New from Prompt&lt;/strong&gt; - generate a workflow from a natural-language description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New from Chat&lt;/strong&gt; - build one conversationally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New from Custom Workflow Builder&lt;/strong&gt; - craft your &lt;em&gt;own&lt;/em&gt; builder UI on ByteChef's APIs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We'll take them one at a time. But every one of them assumes the user has already connected the apps their automation touches - which is exactly what you just shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Sample App Yourself
&lt;/h2&gt;

&lt;p&gt;The sample app needs to know two things: where your ByteChef instance lives, and which Signing Key your backend signs with.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Point the frontend at ByteChef.&lt;/strong&gt; In &lt;code&gt;front-end/.env.local&lt;/code&gt;, set &lt;code&gt;NEXT_PUBLIC_BYTECHEF_APP_BASE_URL&lt;/code&gt;. On &lt;strong&gt;ByteChef Cloud&lt;/strong&gt; that's &lt;code&gt;https://app.bytechef.io&lt;/code&gt;. On a &lt;strong&gt;self-hosted&lt;/strong&gt; instance it's your own URL, and the default &lt;code&gt;http://localhost:8080&lt;/code&gt; already matches a local install, so you can leave it unset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give the backend a Signing Key.&lt;/strong&gt; In ByteChef, open &lt;strong&gt;Embedded → Settings → Signing Keys&lt;/strong&gt;, create a key, and copy it into &lt;code&gt;back-end/.env&lt;/code&gt; as &lt;code&gt;BYTECHEF_PRIVATE_KEY&lt;/code&gt; (shown once) and &lt;code&gt;BYTECHEF_KID&lt;/code&gt;. Create it on the &lt;em&gt;same&lt;/em&gt; instance the frontend points at - a Cloud key won't verify against a self-hosted server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else (environment, demo user id) has a working default. The &lt;a href="https://docs.bytechef.io/platform/embedded/get-started/quick-start/sample-app" rel="noopener noreferrer"&gt;sample app docs&lt;/a&gt; list every variable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>bytechef</category>
      <category>automation</category>
    </item>
    <item>
      <title>Share What You Built: Project &amp; Workflow Templates</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Tue, 08 Sep 2026 19:01:59 +0000</pubDate>
      <link>https://dev.to/bytechef/share-what-you-built-project-workflow-templates-2l98</link>
      <guid>https://dev.to/bytechef/share-what-you-built-project-workflow-templates-2l98</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Templates let you &lt;strong&gt;start from a working automation instead of a blank canvas&lt;/strong&gt; - and let you share your own. Browse the gallery of pre-built &lt;strong&gt;project&lt;/strong&gt; and &lt;strong&gt;workflow&lt;/strong&gt; templates and import one into your workspace, or turn something you built into a &lt;strong&gt;private import link&lt;/strong&gt; that you can disable at any time. Connections never travel with a template, so sharing an automation never shares your credentials.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The best automation you'll ever build is one you don't have to build - because someone already did, and shared it. We wrote about &lt;a href="https://blog.bytechef.io/blogs/import-export-workflows-projects" rel="noopener noreferrer"&gt;import and export&lt;/a&gt; as the foundation for this a while back, and promised template support on top of it. This is that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start From a Working Automation
&lt;/h2&gt;

&lt;p&gt;There are two granularities, and both live behind a gallery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project templates&lt;/strong&gt; - open &lt;strong&gt;Projects → Templates&lt;/strong&gt; to browse pre-built projects. Selecting one imports it as a regular project you can edit, publish, and deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow templates&lt;/strong&gt; - inside a project, browse workflow templates and import one as a new workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each gallery has a search box and a category filter, and every card shows the components the template uses as icons - so you can tell at a glance whether a template touches Slack, Postgres, or an LLM before you commit to it.&lt;/p&gt;

&lt;p&gt;Picking a card takes you to an import page rather than importing blind. That page lists every component the template contains, which is your shopping list: those are the apps you'll need connections for once the automation is in your workspace. When it looks right, &lt;strong&gt;Import now&lt;/strong&gt; drops a copy into your current workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Share What You Built
&lt;/h2&gt;

&lt;p&gt;The other half is publishing. Open the three-dot menu next to a project - or an individual workflow, from its own menu - and choose &lt;strong&gt;Share&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The dialog asks for one thing before it will do anything: a &lt;strong&gt;description&lt;/strong&gt;. It's required, and the export button stays disabled until you write one. That constraint is doing real work - a template with no explanation is a template nobody imports.&lt;/p&gt;

&lt;p&gt;Then click &lt;strong&gt;Export and generate template link&lt;/strong&gt;. The dialog turns into a link card with a &lt;strong&gt;Copy link&lt;/strong&gt; button, and the link looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-instance/import/shared/projects/{uuid}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workflows get the same shape at &lt;code&gt;/import/shared/workflows/{uuid}&lt;/code&gt;. Anyone you hand that link to lands on the same import page described above - the component list, then &lt;strong&gt;Import now&lt;/strong&gt;. They do need an account on a ByteChef instance and a workspace to import into; this is a link for colleagues and collaborators, not an anonymous public download.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Link Stays Yours
&lt;/h2&gt;

&lt;p&gt;Three details make this comfortable to actually use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can turn it off.&lt;/strong&gt; A toggle at the top of the dialog disables the link at any time - the existing URL stops working immediately. Flip it back on later and ByteChef generates a fresh export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It won't drift behind your project silently.&lt;/strong&gt; Keep editing after you've shared, and the dialog tells you that an older version is the one being handed out, and offers &lt;strong&gt;Update template based on the current version&lt;/strong&gt;. The link keeps pointing at whatever you last decided it should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ByteChef doesn't distribute it for you.&lt;/strong&gt; The dialog says so directly: template links are not distributed by the app. Generating a link does not list your project anywhere, does not surface it in the gallery, and does not make it discoverable. You decide who gets the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Travels, and What Doesn't
&lt;/h2&gt;

&lt;p&gt;A shared template is a small zip: a manifest with your description and the project version, the project metadata, and one JSON file per workflow definition. That's the whole payload.&lt;/p&gt;

&lt;p&gt;What is conspicuously &lt;em&gt;not&lt;/em&gt; in there: connections. No API keys, no OAuth tokens, no credentials of any kind. The importer wires up their own accounts against the component list on the import page. This is why sharing an automation is a low-stakes act - you're sharing the shape of the work, never the keys to your systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listing in the Public Gallery
&lt;/h2&gt;

&lt;p&gt;Private links are one route. If you want a template listed in ByteChef's official gallery for anyone to browse, that's a &lt;strong&gt;separate&lt;/strong&gt; action: &lt;strong&gt;Share with Community&lt;/strong&gt;, which opens the public template submission form. The two are genuinely different - one hands a URL to people you choose, the other proposes an addition to a curated catalog.&lt;/p&gt;

&lt;p&gt;Self-hosted instances behind a firewall can point the gallery at a mirror rather than reaching ByteChef's catalog directly; the GitHub proxy configuration in the docs covers it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Sharing Compounds
&lt;/h2&gt;

&lt;p&gt;A workflow you keep to yourself helps one team once. A workflow you template helps everyone who imports it - and they can adapt it, re-share it, and improve on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding&lt;/strong&gt; - new users start from proven automations instead of a tutorial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardization&lt;/strong&gt; - a team's "the right way to do X" becomes a template everyone imports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community&lt;/strong&gt; - the best patterns rise to the top and spread.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Every automation you build is either a one-off or a starting point, and the difference is a description and a click. Templates let the good ones travel - built once, imported wherever they're needed, improved by whoever needs them next - without ever putting your credentials on the move.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built something worth reusing? Share it as a template - or browse the gallery and start from one instead of a blank canvas.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automaton</category>
      <category>bytechef</category>
      <category>agents</category>
    </item>
    <item>
      <title>Somewhere to Keep the Records: Data Tables in ByteChef</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Sun, 30 Aug 2026 09:59:22 +0000</pubDate>
      <link>https://dev.to/bytechef/somewhere-to-keep-the-records-data-tables-in-bytechef-371n</link>
      <guid>https://dev.to/bytechef/somewhere-to-keep-the-records-data-tables-in-bytechef-371n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;strong&gt;Data Tables&lt;/strong&gt; are ByteChef's built-in place to keep records. Create a table with &lt;strong&gt;typed columns&lt;/strong&gt; (string, number, integer, date, date-time, boolean), fill it in a spreadsheet-like grid or &lt;strong&gt;import a CSV&lt;/strong&gt;, and use it from your workflows through &lt;strong&gt;six actions&lt;/strong&gt; (create, get, find, update, delete, clear) and &lt;strong&gt;three triggers&lt;/strong&gt; that start a workflow whenever a row is created, updated, or deleted. Those same six actions are available to &lt;strong&gt;AI agents as tools&lt;/strong&gt;, so an agent can look things up and write rows for you. Your Development, Staging, and Production rows stay in &lt;strong&gt;separate tables&lt;/strong&gt;, so test data never leaks into live automations. No database to set up, no spreadsheet to babysit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's a question every automation project runs into by week two: &lt;em&gt;"where do we keep the records?"&lt;/em&gt; The leads the workflow has already processed. The subscriptions to check against. The running list of approvals, orders, escalations. Workflow steps don't remember anything between runs; the data has to live &lt;em&gt;somewhere&lt;/em&gt;, and the classic answers are both awkward. A spreadsheet bolted onto the side is easy but fragile: no real types, no stable way to point at a row, rate limits, and an API that was never meant to be your database. An actual database is solid but heavy: someone has to provision it, design it, secure it, and write the plumbing for every read and write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Tables&lt;/strong&gt; are ByteChef's answer to that gap: tables that live inside the platform, as easy as a spreadsheet from the front, as dependable as a database underneath, and already connected to everything else your workflows do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set One Up in a Minute
&lt;/h2&gt;

&lt;p&gt;A Data Table starts the way you'd hope. Open &lt;strong&gt;Data Tables&lt;/strong&gt; in the Automation workspace, click &lt;strong&gt;New Table&lt;/strong&gt;, give it a name, and add your columns; each one gets a type: &lt;strong&gt;string, number, integer, date, date-time, or boolean&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From there you get a familiar grid. Add a row by typing into the empty one at the bottom. Click a cell to edit it, and you get the right editor for the column: a checkbox for booleans, a date picker for dates, a number field for numbers. Select rows with checkboxes to delete them. Longer tables load more rows as you scroll.&lt;/p&gt;

&lt;p&gt;Already have the data in a spreadsheet? &lt;strong&gt;Import a CSV&lt;/strong&gt; from the table's &lt;strong&gt;⋮&lt;/strong&gt; menu and the rows land in the matching columns. The same menu &lt;strong&gt;exports&lt;/strong&gt; a CSV back out, so nothing you put in is trapped. Once you've got a few tables, &lt;strong&gt;tags&lt;/strong&gt; keep the list organized.&lt;/p&gt;

&lt;p&gt;Picking a type per column sounds like a small chore, and it pays for itself almost immediately: a date column is a real date rather than text that usually parses, numbers sort like numbers instead of alphabetically, and a boolean is true or false rather than "yes", "Y", and "TRUE" all living in the same column. Every row also gets its own &lt;strong&gt;id&lt;/strong&gt; that never changes, so you can hold onto a specific row and come back to it later, something row numbers in a spreadsheet can never promise you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Data Stays Test Data
&lt;/h2&gt;

&lt;p&gt;Here's the part people are usually surprised by. A Data Table isn't one shared pile of rows; ByteChef keeps a &lt;strong&gt;separate set of rows for each environment&lt;/strong&gt;. The table you filled with junk while building in Development is genuinely not the table your Production workflow reads from.&lt;/p&gt;

&lt;p&gt;That means you can test a workflow that inserts, updates, and deletes records without a moment's worry about what it's touching, then promote it and watch it run against real data. No "_TEST" prefix conventions, no second copy of a spreadsheet that drifts out of sync, no accidentally emailing your actual customers because a test row was sitting in the live sheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six Actions: Tables in Your Workflows
&lt;/h2&gt;

&lt;p&gt;Add the &lt;strong&gt;Data Table&lt;/strong&gt; component to a workflow and you can read and write any of your tables. There's no connection to set up and nothing to authenticate against; the tables are part of the platform, so they're just &lt;em&gt;there&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create Records&lt;/strong&gt; - add one row or many at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get Record&lt;/strong&gt; - fetch a specific row by its id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find Records&lt;/strong&gt; - list rows from a table, with limit and offset so you can page through them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Record&lt;/strong&gt; - change the values on a row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete Records&lt;/strong&gt; - remove rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear Table&lt;/strong&gt; - empty a table in one step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick your table and the action's inputs reshape themselves around that table's columns, so you're filling in &lt;em&gt;Email&lt;/em&gt; and &lt;em&gt;Status&lt;/em&gt;, not guessing at field names.&lt;/p&gt;

&lt;p&gt;That covers the patterns automations lean on constantly: &lt;em&gt;accumulate&lt;/em&gt; ("append every order this workflow processed"), &lt;em&gt;enrich later&lt;/em&gt; ("store the record now, update it when the webhook finally lands"), &lt;em&gt;hand off&lt;/em&gt; ("one workflow writes the row, another picks it up"), and &lt;em&gt;keep a log&lt;/em&gt; ("record what this run did, so there's something to look at on Monday").&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Triggers: Tables That Start Workflows
&lt;/h2&gt;

&lt;p&gt;This is where Data Tables stop being just storage. Most built-in tables in other tools are passive: you read and write them, and that's the end of it. ByteChef's tables can &lt;em&gt;start&lt;/em&gt; a workflow. The component ships three triggers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Record Created&lt;/strong&gt; - a new row was added.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record Updated&lt;/strong&gt; - a row changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record Deleted&lt;/strong&gt; - a row was removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which quietly changes the shape of what you can build. A table stops being a filing cabinet at the edge of your automations and becomes the thing they're organized around. One workflow adds a row; three other workflows react to it, and the one doing the writing doesn't need to know they exist.&lt;/p&gt;

&lt;p&gt;Say an intake workflow drops a new order into a table. A fulfillment workflow, a customer-notification workflow, and a reporting workflow each start themselves on &lt;strong&gt;Record Created&lt;/strong&gt;, independently. Want to add a fourth later? Build it and switch it on. Nothing else needs touching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents Can Use Them Too
&lt;/h2&gt;

&lt;p&gt;Data Tables aren't only for workflows you draw by hand. Every one of those six actions is also available to &lt;strong&gt;AI agents as a tool&lt;/strong&gt;, so an agent can look up a record, add one, update one, or clear a table, using exactly the same operations your workflows use.&lt;/p&gt;

&lt;p&gt;That means a table can be shared ground between the two ways of working. A scheduled workflow keeps a table of open tickets up to date; an agent you're chatting with reads that same table to answer "what's still unresolved?" and writes a row back when it takes something on. Same data, same rules, no separate copy to keep in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Fits
&lt;/h2&gt;

&lt;p&gt;Data Tables aren't trying to replace your production database; they're for the working data &lt;em&gt;of your automations&lt;/em&gt;. A rough guide:&lt;/p&gt;

&lt;p&gt;Data Tables are a good fit when…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your workflows need to share data or remember state between runs.&lt;/li&gt;
&lt;li&gt;You’re tracking processed items, queues, lookups, or workflow logs.&lt;/li&gt;
&lt;li&gt;You want one workflow to react when data changes.&lt;/li&gt;
&lt;li&gt;Your dataset is relatively small and primarily written by workflows.
Consider something else when…&lt;/li&gt;
&lt;li&gt;You need a system of record for your product → use a real database.&lt;/li&gt;
&lt;li&gt;People need to collaboratively edit free-form content → use a spreadsheet or document.&lt;/li&gt;
&lt;li&gt;You’re moving or syncing large datasets between systems → DataStream (coming soon).&lt;/li&gt;
&lt;li&gt;Your knowledge is stored in documents rather than rows → use a &lt;a href="https://blog.bytechef.io/blogs/bytechef-knowledge-base" rel="noopener noreferrer"&gt;Knowledge Base&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things worth knowing before you commit. A Data Table is &lt;strong&gt;not a transactional system of record&lt;/strong&gt;: there's no cross-row transaction or multi-writer locking, so if several outside systems are writing the same data at the same time, point your workflow at a real database instead. And it's sized for small datasets, not warehouses: think lookup tables, queues, flags, and processed-item lists rather than millions of rows.&lt;/p&gt;

&lt;p&gt;That right-hand column is really a tour of ByteChef's storage story: &lt;strong&gt;Data Tables&lt;/strong&gt; for structured rows, the &lt;strong&gt;&lt;a href="https://blog.bytechef.io/blogs/bytechef-knowledge-base" rel="noopener noreferrer"&gt;Knowledge Base&lt;/a&gt;&lt;/strong&gt; for searchable documents, and &lt;strong&gt;DataStream (coming soon)&lt;/strong&gt; for moving bulk data between systems. Three shapes of data, three purpose-built homes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Remembering things is the unglamorous half of automation. Every workflow beyond the trivial needs to keep track of something, and &lt;em&gt;how well&lt;/em&gt; it remembers (real types or everything-as-text, stable ids or shifting row numbers, reacting to changes or polling for them) quietly decides how far that automation can grow before it starts to hurt.&lt;/p&gt;

&lt;p&gt;Data Tables give that job a proper home: tables you fill in like a spreadsheet, with types that hold, ids that stay put, test data kept away from live data, six actions to read and write them from your workflows or hand to an agent, and three triggers so a change to a row can set the next workflow going. No database to provision, no sheet to babysit, just somewhere real for your workflows to keep what they know.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Got a workflow juggling state in a spreadsheet? Open &lt;a href="https://app.bytechef.io" rel="noopener noreferrer"&gt;ByteChef&lt;/a&gt;, create a **Data Table&lt;/em&gt;&lt;em&gt;, and give your records somewhere better to live.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>bytechef</category>
      <category>automation</category>
      <category>agents</category>
    </item>
    <item>
      <title>Agent Skills in ByteChef: Package Know-How Once, Give It to Every Agent</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Thu, 27 Aug 2026 16:19:30 +0000</pubDate>
      <link>https://dev.to/bytechef/agent-skills-in-bytechef-package-know-how-once-give-it-to-every-agent-11i1</link>
      <guid>https://dev.to/bytechef/agent-skills-in-bytechef-package-know-how-once-give-it-to-every-agent-11i1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;strong&gt;Agent Skills&lt;/strong&gt; are self-contained packages of instructions, scripts, and resources - built around a &lt;code&gt;SKILL.md&lt;/code&gt; file - that agents discover and load &lt;em&gt;on demand&lt;/em&gt;. The Spring AI team &lt;a href="https://spring.io/blog/2026/01/13/spring-ai-generic-agent-skills" rel="noopener noreferrer"&gt;just added generic support for them in Java&lt;/a&gt;. &lt;a href="https://www.bytechef.io" rel="noopener noreferrer"&gt;ByteChef&lt;/a&gt;, which runs on Spring AI under the hood, goes a step further: a built-in &lt;strong&gt;Skills&lt;/strong&gt; workspace for creating, editing, and sharing skills; &lt;strong&gt;isolated GraalVM execution&lt;/strong&gt; for bundled scripts; and a &lt;strong&gt;Skills Tool&lt;/strong&gt; that plugs any skill into any AI Agent. Write the know-how once, reuse it everywhere - no code, no redeploys.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's a moment every agent builder hits: your AI Agent works, but making it &lt;em&gt;good at your job&lt;/em&gt; means feeding it more and more instructions. The refund policy. The report format. The naming conventions. The seventeen edge cases finance cares about. Everything lands in the system prompt, which grows into an unmaintainable wall of text that every request pays for in tokens - whether it's relevant or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Skills&lt;/strong&gt; are the answer to that problem, and they're having a moment. Anthropic introduced the pattern and its &lt;code&gt;SKILL.md&lt;/code&gt; convention; it quickly spread across the ecosystem. In January, the Spring AI team published &lt;a href="https://spring.io/blog/2026/01/13/spring-ai-generic-agent-skills" rel="noopener noreferrer"&gt;Agent Skills in Spring AI&lt;/a&gt; - the first post in their agentic patterns series - showing how to wire skills into any Java application with &lt;code&gt;SkillsTool&lt;/code&gt;, file-system tools, and shell tools.&lt;/p&gt;

&lt;p&gt;If you've read our previous posts on &lt;a href="https://blog.bytechef.io/blogs/spring-ai-agent" rel="noopener noreferrer"&gt;the AI Agent component&lt;/a&gt; and &lt;a href="https://blog.bytechef.io/blogs/bytechef-agentic-patterns" rel="noopener noreferrer"&gt;agentic workflow patterns&lt;/a&gt;, you know where this is going: ByteChef is built &lt;strong&gt;on top of Spring AI&lt;/strong&gt;, and skills are no exception. But where Spring AI gives Java developers the building blocks, ByteChef ships the whole thing as a product - skills management, a visual editor, and one-click attachment to any agent.&lt;/p&gt;

&lt;p&gt;Let's dig in.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is an Agent Skill?
&lt;/h2&gt;

&lt;p&gt;A skill is a folder. That's genuinely the whole format - and it's why the pattern is spreading so fast.&lt;/p&gt;

&lt;p&gt;At the root sits a &lt;code&gt;SKILL.md&lt;/code&gt; file: YAML frontmatter with metadata on top, Markdown instructions below. Around it, optional supporting files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email-digest/
├── SKILL.md          ← metadata + instructions (required)
├── scripts/          ← executable helpers
│   └── run_digest.py
├── references/       ← extra documentation, loaded on demand
└── assets/           ← templates and other resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what a real &lt;code&gt;SKILL.md&lt;/code&gt; looks like - this is the email-digest skill we'll use as the running example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-digest&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;Fetches unread emails from Gmail, summarizes key points using AI,&lt;/span&gt;
    &lt;span class="s"&gt;and sends the digest back to the user's Gmail inbox. Use when the&lt;/span&gt;
    &lt;span class="s"&gt;user asks to summarize their unread emails or get an email digest.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Email Digest Skill&lt;/span&gt;

&lt;span class="gu"&gt;## What It Does&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Fetches unread emails from Gmail (up to a configurable limit)
&lt;span class="p"&gt;2.&lt;/span&gt; Uses an LLM to summarize key points per email
&lt;span class="p"&gt;3.&lt;/span&gt; Sends a formatted digest email back to the user's Gmail

&lt;span class="gu"&gt;## How to Run&lt;/span&gt;

Call the &lt;span class="sb"&gt;`email_digest_run_digest`&lt;/span&gt; tool. It runs the bundled
&lt;span class="sb"&gt;`scripts/run_digest.py`&lt;/span&gt;, which fetches the unread mail, summarizes
it, and sends the digest.

&lt;span class="gu"&gt;## Output&lt;/span&gt;

A single HTML email sent to the recipient containing a summary
of every unread message, grouped by sender.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice. First, the &lt;strong&gt;description&lt;/strong&gt; does double duty: it tells humans what the skill is for, and it tells the &lt;em&gt;agent&lt;/em&gt; when to reach for it - that last sentence ("Use when the user asks to…") is discovery guidance, not documentation fluff. Second, the instructions are written &lt;em&gt;for the agent&lt;/em&gt;, in imperative, unambiguous language. A skill is essentially an onboarding document for a very fast, very literal new hire.&lt;/p&gt;

&lt;p&gt;Because the format is plain Markdown in a folder, skills are &lt;strong&gt;portable&lt;/strong&gt;. The same convention is used by Claude Code, by Spring AI's implementation, and by ByteChef - a skill you write in one place can travel to the others.&lt;/p&gt;




&lt;h2&gt;
  
  
  Progressive Disclosure: Why Skills Scale Where System Prompts Don't
&lt;/h2&gt;

&lt;p&gt;The clever part of the skills pattern isn't the file format - it's &lt;em&gt;when&lt;/em&gt; the content gets loaded. Skills use &lt;strong&gt;progressive disclosure&lt;/strong&gt;, revealing themselves to the agent in three phases:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fezta496etfhy3khrej7e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fezta496etfhy3khrej7e.png" alt=" " width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discovery.&lt;/strong&gt; By default, the agent only sees each skill's &lt;em&gt;name and description&lt;/em&gt; - a few dozen tokens per skill. This is what lets an agent carry dozens of skills without drowning its context window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activation.&lt;/strong&gt; When a request matches a skill's description, the full &lt;code&gt;SKILL.md&lt;/code&gt; instructions load into context - only then, and only for that skill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution.&lt;/strong&gt; Referenced Markdown files and script outputs enter the picture on demand, while the skill runs, and not a moment earlier.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compare that with the stuff-everything-into-the-system-prompt approach, where the refund policy is in context while the user asks about the weather. Progressive disclosure means you pay for know-how &lt;strong&gt;only when it's being used&lt;/strong&gt; - which is exactly what makes a library of specialized skills economically viable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Skills at Runtime: The Skills Tool
&lt;/h2&gt;

&lt;p&gt;On the ByteChef side, an agent gets access to skills through the &lt;strong&gt;Skills Tool&lt;/strong&gt; - one of the tool options in the AI Agent's &lt;strong&gt;Tools&lt;/strong&gt; slot, alongside &lt;a href="https://blog.bytechef.io/blogs/spring-ai-agent" rel="noopener noreferrer"&gt;component actions, MCP tools, and sub-agents&lt;/a&gt;. Attach it, and the agent can discover and invoke any skill in your workspace.&lt;/p&gt;

&lt;p&gt;The runtime flow looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukwvsdc3c1kneashc7bg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukwvsdc3c1kneashc7bg.png" alt=" " width="800" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note what's &lt;em&gt;absent&lt;/em&gt; from that picture: routing logic. You don't configure which requests trigger which skill - the agent matches the user's intent against skill descriptions on its own. Add a new skill to the workspace and every agent with the Skills Tool can start using it immediately.&lt;/p&gt;

&lt;p&gt;When a skill bundles scripts, ByteChef doesn't hand the agent a shell. It walks the skill's &lt;code&gt;scripts/&lt;/code&gt; folder and turns every script that exports a &lt;code&gt;perform&lt;/code&gt; function into &lt;strong&gt;a tool of its own&lt;/strong&gt; - so &lt;code&gt;scripts/run_digest.py&lt;/code&gt; inside the &lt;code&gt;email-digest&lt;/code&gt; skill becomes a callable tool named &lt;code&gt;email_digest_run_digest&lt;/code&gt;. JavaScript, Python, Ruby, and Java all work, and each one is executed by a GraalVM polyglot engine rather than a subprocess. The agent sees a named tool it can call, not a command line it can compose.&lt;/p&gt;

&lt;p&gt;That distinction matters, because the Spring AI post is refreshingly honest about its own trade-off here: in the DIY setup, &lt;em&gt;"scripts execute directly on your local machine without sandboxing,"&lt;/em&gt; and the recommended mitigation is to containerize your whole application. A ByteChef skill script runs in a polyglot context with no host filesystem, no network sockets, and no native access - it reaches the outside world only through &lt;code&gt;context.component&lt;/code&gt;, the same action layer the rest of the platform runs on. It is not a resource sandbox: there are no CPU or memory ceilings on guest code, so skill scripts remain code worth reading before you attach them. But the ambient authority a loose &lt;code&gt;python3&lt;/code&gt; would inherit simply isn't there. Combined with &lt;a href="https://blog.bytechef.io/blogs/spring-ai-agent" rel="noopener noreferrer"&gt;Guardrails&lt;/a&gt; on the agent itself, you get a controlled execution story without building any of it.&lt;/p&gt;

&lt;p&gt;There's a neat side effect. Because those scripts are just files, ByteChef reads them &lt;em&gt;before&lt;/em&gt; the workflow ever runs: it scans each one for &lt;code&gt;context.component.&amp;lt;name&amp;gt;.&amp;lt;action&amp;gt;(…)&lt;/code&gt; calls and surfaces a connection field for every component it recognizes. Bundle a script that calls &lt;code&gt;context.component.gmail.*&lt;/code&gt; and the Skills Tool node asks you for a Gmail connection. The skill declares its own dependencies in code, rather than in metadata you have to remember to keep in sync.&lt;/p&gt;

&lt;p&gt;And because model access goes through Spring AI's provider abstraction, skills are &lt;strong&gt;model-portable&lt;/strong&gt;: the same skill works whether the agent runs on GPT-4o, Claude, Gemini, or a local Ollama model. Define once, run anywhere - swap the model from a dropdown later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Built-In Skills Management
&lt;/h2&gt;

&lt;p&gt;In Spring AI, skills live in folders on the classpath or filesystem, next to your application code. ByteChef gives them a home in the product instead: the &lt;strong&gt;Skills&lt;/strong&gt; area, right in the AI section of the workspace.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl5y0j0adv8xa82hgd0c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl5y0j0adv8xa82hgd0c.png" alt=" " width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every skill in the list shows its name, its description (the same one agents use for discovery), and when it was last modified. From here you can search, create, and manage the whole library. Two ways to create a skill are available today, with a third on the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write instructions.&lt;/strong&gt; Give it a name and a description, write what it should do in plain text, and ByteChef packages it into the &lt;code&gt;SKILL.md&lt;/code&gt; structure for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload.&lt;/strong&gt; Drag in a &lt;code&gt;.skill&lt;/code&gt; archive, a &lt;code&gt;.zip&lt;/code&gt;, or a bare &lt;code&gt;.md&lt;/code&gt; file. This is the import path for skills built elsewhere - including skills from the wider &lt;code&gt;SKILL.md&lt;/code&gt; ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create with AI&lt;/strong&gt; &lt;em&gt;(coming soon)&lt;/em&gt;. Describe the capability ("a skill that summarizes my unread Gmail every morning") and let ByteChef's Copilot draft the skill's structure, metadata, and instructions. This path rides on the AI Copilot, which is on the upcoming release track rather than in the current release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once created, a skill opens in the built-in editor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz2lgtxoqup8rgqr9e5n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz2lgtxoqup8rgqr9e5n.png" alt=" " width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the email-digest skill from earlier, as it actually looks in ByteChef. On the left, the file tree - &lt;code&gt;SKILL.md&lt;/code&gt; plus a &lt;code&gt;scripts/&lt;/code&gt; folder holding the Python helper. The main pane renders the frontmatter as a metadata table above the formatted instructions - ByteChef validates and stores &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;, and displays whatever other keys the file carries verbatim, so a skill imported from another tool keeps its own metadata intact. A toggle switches between this preview and a full &lt;strong&gt;source editor&lt;/strong&gt; that picks its syntax highlighting from the file extension - Markdown, Python, JavaScript, TypeScript, Java, JSON, YAML, HTML, CSS, SQL - so you can edit any file in the package. Save, and the change is live - every agent using the skill picks it up on its next run, no rebuild, no redeploy.&lt;/p&gt;

&lt;p&gt;Need to move a skill between workspaces, share it with a teammate, or keep a snapshot before a big edit? &lt;strong&gt;Download&lt;/strong&gt; it as a &lt;code&gt;.skill&lt;/code&gt; archive and it becomes a file you can version, review, and re-upload anywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Payoff: One Skill, Many Agents
&lt;/h2&gt;

&lt;p&gt;Here's where the managed approach compounds. Because skills are decoupled from any single agent, they form a &lt;strong&gt;shared capability library&lt;/strong&gt; for your whole workspace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The support agent, the sales agent, and the internal helpdesk agent can all carry the same &lt;code&gt;refund-policy&lt;/code&gt; skill. Update the policy once; all three agents follow the new rules on their next conversation.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;release-notes&lt;/code&gt; skill encodes your changelog format and tone. Attach it to the engineering agent that drafts notes from merged PRs &lt;em&gt;and&lt;/em&gt; to the marketing agent that turns them into announcement posts.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;email-digest&lt;/code&gt; skill above doesn't care which agent invokes it - any agent with the Skills Tool and a Gmail connection can produce the digest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same "build once, reuse everywhere" argument the Spring AI post makes with the composability of skills - except the unit of reuse isn't a folder you copy between codebases; it's a managed object every agent in the workspace can already see.&lt;/p&gt;

&lt;p&gt;And you can verify all of it before it touches production: open the agent in the editor, and the &lt;a href="https://blog.bytechef.io/blogs/spring-ai-agent" rel="noopener noreferrer"&gt;Agent Playbook&lt;/a&gt; lets you chat with your in-progress configuration. Ask something that should trigger the skill, and the expandable tool-call cards show the skill being discovered and invoked - which tool ran, with what inputs, returning what output. If the agent picks the wrong skill (usually a sign your description is too vague), you'll see it in seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Skills, System Prompts, or RAG?
&lt;/h2&gt;

&lt;p&gt;Skills join two other ways of shaping agent behavior, and it's worth being deliberate about which knowledge goes where:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;th&gt;Loaded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;System prompt&lt;/td&gt;
&lt;td&gt;Identity and ground rules: who the agent is, tone, hard constraints&lt;/td&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;td&gt;Facts: your documents, tickets, product data - &lt;em&gt;what is true&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Per query, by similarity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skills&lt;/td&gt;
&lt;td&gt;Procedures: how to perform a task, step by step - &lt;em&gt;how it's done&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;On demand, by intent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful rule of thumb: if you're writing "always" or "never," it belongs in the system prompt. If you're writing "our Q3 pricing is…," that's a document - put it in &lt;a href="https://blog.bytechef.io/blogs/spring-ai-agent" rel="noopener noreferrer"&gt;RAG or the Knowledge Base&lt;/a&gt;. If you're writing "first do this, then that, format the result like so," you're writing a skill.&lt;/p&gt;

&lt;p&gt;The three compose naturally on one agent: a lean system prompt for identity, RAG for grounding, and a library of skills for the procedures - each loaded at the moment it earns its context cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Inherit from Spring AI - and What ByteChef Adds
&lt;/h2&gt;

&lt;p&gt;The Spring AI team closes their post with the pattern's strengths, and being built on Spring AI, ByteChef inherits all of them: &lt;strong&gt;LLM portability&lt;/strong&gt; (define a skill once, run it on any supported provider), &lt;strong&gt;reusability and composability&lt;/strong&gt; (share skills across projects, version them, extend them), and &lt;strong&gt;no vendor lock-in&lt;/strong&gt; (the common &lt;code&gt;SKILL.md&lt;/code&gt; convention travels).&lt;/p&gt;

&lt;p&gt;They're equally upfront about the limitations. Their list runs to four items, and ByteChef's managed layer closes two of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Scripts execute directly on the local machine without sandboxing&lt;/em&gt; → in ByteChef, each script becomes a tool running inside a &lt;strong&gt;GraalVM polyglot context&lt;/strong&gt; with no host filesystem, network, or native access.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Script execution requires pre-installed runtimes&lt;/em&gt; → the polyglot engine ships &lt;strong&gt;in the platform&lt;/strong&gt;; JavaScript, Python, Ruby, and Java run with nothing to install.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other two are honest gaps in both. There's still &lt;strong&gt;no built-in human approval step&lt;/strong&gt; before a skill's script runs, and &lt;strong&gt;skill versioning&lt;/strong&gt; is whatever you make of it - download a &lt;code&gt;.skill&lt;/code&gt; archive and commit it alongside your code, but nothing versions skills for you in-product.&lt;/p&gt;

&lt;p&gt;Two further differences don't appear on their list at all, because they're product concerns rather than framework ones: skills live in a &lt;strong&gt;managed workspace&lt;/strong&gt; with search and a visual editor instead of on the filesystem next to your code, and wiring is one &lt;strong&gt;Skills Tool&lt;/strong&gt; attached to an agent instead of dependencies and configuration code.&lt;/p&gt;

&lt;p&gt;Same foundation, same format, same portability - minus most of the infrastructure work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Best Practices for Writing Skills
&lt;/h2&gt;

&lt;p&gt;A few habits that separate skills agents &lt;em&gt;reliably&lt;/em&gt; pick up from skills that gather dust:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat the description as an API.&lt;/strong&gt; It's the only thing the agent sees at discovery time. State what the skill does &lt;em&gt;and&lt;/em&gt; when to use it: "…Use when the user asks to summarize their unread emails." Vague descriptions are the number-one cause of skills never firing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One job per skill.&lt;/strong&gt; "Handle refunds" and "write release notes" are two skills, not one. Small skills are easier for the agent to match and for you to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep &lt;code&gt;SKILL.md&lt;/code&gt; lean.&lt;/strong&gt; The instructions load in full at activation, so they should carry the workflow - push long reference material into &lt;code&gt;references/&lt;/code&gt; files the agent reads only when needed. Progressive disclosure only saves tokens if you let it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer scripts for deterministic work.&lt;/strong&gt; If a step is exact - parsing, formatting, calling an API in a precise way - a bundled script beats prose instructions. The model decides &lt;em&gt;when&lt;/em&gt;; the script guarantees &lt;em&gt;how&lt;/em&gt;. Give each script under &lt;code&gt;scripts/&lt;/code&gt; a &lt;code&gt;perform&lt;/code&gt; function: that's what ByteChef looks for when deciding which files to expose as tools, and a script without one is quietly skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test in the Playbook first.&lt;/strong&gt; Phrase requests the way real users would, watch which skill activates, and tune descriptions until discovery is reliable.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Agent Skills solve an unglamorous but decisive problem: where does an agent's know-how &lt;em&gt;live&lt;/em&gt;? Not in a bloated system prompt, not scattered across duplicated agent configs - in small, portable, on-demand packages that any agent can pick up.&lt;/p&gt;

&lt;p&gt;Spring AI brought that pattern to the Java world as a library. ByteChef, standing on Spring AI's shoulders, turns it into infrastructure you don't have to build: managed storage, a visual editor, isolated script execution, and one-click attachment to any agent - with the Agent Playbook to prove it all works before you ship, and AI-assisted authoring on the way.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Got a procedure your team explains over and over? That's your first skill. Open &lt;a href="https://app.bytechef.io" rel="noopener noreferrer"&gt;ByteChef&lt;/a&gt;, head to the Skills area, and teach it once.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
