<?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>Your AI agent skips steps. Give it a todo list.</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:28:26 +0000</pubDate>
      <link>https://dev.to/bytechef/your-ai-agent-skips-steps-give-it-a-todo-list-28an</link>
      <guid>https://dev.to/bytechef/your-ai-agent-skips-steps-give-it-a-todo-list-28an</guid>
      <description>&lt;p&gt;You give an agent a real job: "audit these ten endpoints, fix the auth bug in each, run the tests, write up what changed."&lt;/p&gt;

&lt;p&gt;It starts well. Six endpoints fixed, some tests run, a tidy summary. The last four endpoints? Never touched.&lt;/p&gt;

&lt;p&gt;No crash. No error. It just lost track.&lt;/p&gt;

&lt;p&gt;If you've built anything agentic, you've seen this. Here's a small tool that fixes most of it, and how we run it in &lt;a href="https://github.com/bytechefhq/bytechef" rel="noopener noreferrer"&gt;ByteChef&lt;/a&gt;, an open-source workflow automation and AI agent platform built on Java and Spring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents drop steps
&lt;/h2&gt;

&lt;p&gt;Language models pay less attention to whatever sits in the middle of a long context. As an agent works, its context fills with tool calls and results, and the original plan drifts into that middle zone.&lt;/p&gt;

&lt;p&gt;The Spring AI team covered this in their &lt;a href="https://spring.io/blog/2026/01/20/spring-ai-agentic-patterns-3-todowrite" rel="noopener noreferrer"&gt;TodoWrite post&lt;/a&gt; and borrowed the fix from Claude Code: don't keep the plan in your head. Write it down as a checklist and keep it current.&lt;/p&gt;

&lt;h2&gt;
  
  
  The TodoWrite tool
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;TodoWriteTool&lt;/code&gt; lives in the &lt;code&gt;spring-ai-agent-utils&lt;/code&gt; library. The model sees a tool called &lt;code&gt;TodoWrite&lt;/code&gt;. Every call sends the &lt;strong&gt;whole&lt;/strong&gt; list, and each item has three fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;content&lt;/code&gt; - the task as an instruction: "Update the Spring Boot version"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;activeForm&lt;/code&gt; - the same task while it runs: "Updating the Spring Boot version"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt; - &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;in_progress&lt;/code&gt; or &lt;code&gt;completed&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule does most of the work: &lt;strong&gt;only one task can be &lt;code&gt;in_progress&lt;/code&gt; at a time.&lt;/strong&gt; The tool rejects a list with two tasks in progress and tells the model to finish the current one first. That forces the agent to work in order instead of starting everything at once.&lt;/p&gt;

&lt;p&gt;A few more details from the tool's instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use it for jobs with 3+ distinct steps, not for single simple requests. The agent decides when a job needs a plan.&lt;/li&gt;
&lt;li&gt;Only mark a task &lt;code&gt;completed&lt;/code&gt; when it's fully done. If tests fail or the work is partial, keep it &lt;code&gt;in_progress&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If something blocks a task, add a new task for the blocker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why writing it down works
&lt;/h2&gt;

&lt;p&gt;A checklist only helps if the agent keeps reading it. That's the real trick.&lt;/p&gt;

&lt;p&gt;Each &lt;code&gt;TodoWrite&lt;/code&gt; call carries the full current list in its arguments. During a run, the agent's tool-calling loop keeps every tool call and result in the conversation. So whenever the model picks its next step, the latest list is right there: three done, one in progress, six pending.&lt;/p&gt;

&lt;p&gt;The plan can't fall out of attention because it gets written again on every update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring AI vs. ByteChef setup
&lt;/h2&gt;

&lt;p&gt;In plain Spring AI, you wire this up yourself: add the tool to a &lt;code&gt;ChatClient&lt;/code&gt;, configure &lt;code&gt;MessageChatMemoryAdvisor&lt;/code&gt; and &lt;code&gt;ToolCallAdvisor&lt;/code&gt; so tool messages actually reach the model, and add chat memory if you want follow-ups.&lt;/p&gt;

&lt;p&gt;In ByteChef, the AI Agent component already runs the tool-calling loop and keeps tool messages in the conversation. The Todo Write Tool is one element you drop into the agent's Tools slot. Here's the entire wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;TodoWriteTool&lt;/span&gt; &lt;span class="n"&gt;todoWriteTool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TodoWriteTool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ToolCallbackProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolCallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todoWriteTool&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The library does the work, the AI Agent provides the loop.&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;Spring AI&lt;/th&gt;
&lt;th&gt;ByteChef&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add the tool&lt;/td&gt;
&lt;td&gt;In code, on a &lt;code&gt;ChatClient&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Todo Write Tool element on an AI Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool messages reach the model&lt;/td&gt;
&lt;td&gt;Configure advisors&lt;/td&gt;
&lt;td&gt;Built into the agent loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat memory&lt;/td&gt;
&lt;td&gt;Add it yourself&lt;/td&gt;
&lt;td&gt;Add a Chat Memory element&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same tool, same three statuses, same one-in-progress rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it: a Spring Boot 4 migration planner
&lt;/h2&gt;

&lt;p&gt;Spring Boot 4 migrations are a good stress test. Lots of small steps (Java version, dependencies, Jakarta changes, config properties, tests), and skipping one is easy.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.bytechef.io/workflow-templates/todo-list-spring-boot-migration-planner" rel="noopener noreferrer"&gt;Spring Boot 4 migration planner template&lt;/a&gt; is three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New Chat Request&lt;/strong&gt; - a message arrives in the workflow chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Agent&lt;/strong&gt; - an Anthropic model, Chat Memory for follow-ups, and the Todo Write Tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reply&lt;/strong&gt; - the answer goes back to the chat.&lt;/li&gt;
&lt;/ol&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%2Fzen2y1b8hque72o3gq9b.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%2Fzen2y1b8hque72o3gq9b.png" alt="The Spring Boot 4 migration planner template in the ByteChef editor" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The system prompt does the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a senior Java engineer. For any request with 3 or more distinct
steps, first create a todo list with the TodoWrite tool. Keep exactly one
task in_progress at a time, mark each task completed as soon as it is done,
and update the list after every step. Finish with a short summary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Import the template into a project.&lt;/li&gt;
&lt;li&gt;Add your Anthropic connection to the model under the AI Agent.&lt;/li&gt;
&lt;li&gt;Open the workflow chat and ask for something multi-step, like: &lt;em&gt;"Plan and walk me through migrating a Spring Boot 3.5 service on Java 17 to Spring Boot 4."&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before answering, the agent calls &lt;code&gt;TodoWrite&lt;/code&gt; with a plan. Exact tasks depend on the model, but expect something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Upgrade the service to a supported Java version
[ ] Update the Spring Boot version and the dependency BOM
[ ] Fix renamed and removed configuration properties
[ ] Update tests and run the build
[ ] Summarize the changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the first &lt;code&gt;TodoWrite&lt;/code&gt; call from a real test run, from the AI Agent's Logs tab. One task in progress, the rest pending:&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%2Fs0bkxi8wclq45y2t062g.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%2Fs0bkxi8wclq45y2t062g.png" alt="The AI Agent's logs showing a TodoWrite call with one task in_progress and the rest pending" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then it works through the list one task at a time, calls &lt;code&gt;TodoWrite&lt;/code&gt; after each step, and ends with a summary. The Agent Playbook in the editor shows every update as a tool-call card, so you can watch the list change.&lt;/p&gt;

&lt;p&gt;If the agent skips the list on a big job, make the request more explicit about its steps or tighten the system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it earns its place
&lt;/h2&gt;

&lt;p&gt;It looks almost too simple to matter. On long tasks it's one of the most useful tools you can give an agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It fights lost-in-the-middle by design.&lt;/strong&gt; The plan is rewritten on every update, so no step quietly disappears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One task in progress keeps the agent focused.&lt;/strong&gt; It finishes what it started instead of leaving half-done work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It makes the work visible.&lt;/strong&gt; You see what's done and what's next. That's the fastest way to check the agent understood the job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also pairs well with the other agent utils: File System, Shell, Grep and Glob tools give an agent hands. The todo list gives it a plan to use them on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The tools that make agents reliable are rarely the flashy ones. TodoWrite is a checklist, but one the agent writes itself and re-reads at every step. That's exactly what a long task needs.&lt;/p&gt;

&lt;p&gt;Got an agent that drops steps on long jobs? Import the &lt;a href="https://www.bytechef.io/workflow-templates/todo-list-spring-boot-migration-planner" rel="noopener noreferrer"&gt;migration planner template&lt;/a&gt;, add your Anthropic connection, and give it a ten-step job.&lt;/p&gt;

&lt;p&gt;This is Part 3 of our AI Agentic Patterns series. &lt;a href="https://blog.bytechef.io/blogs/bytechef-agent-skills" rel="noopener noreferrer"&gt;Part 1 covered Agent Skills&lt;/a&gt;. If you like where ByteChef is going, a star on &lt;a href="https://github.com/bytechefhq/bytechef" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps a lot.&lt;/p&gt;

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

</description>
      <category>ai</category>
      <category>java</category>
      <category>opensource</category>
      <category>agents</category>
    </item>
    <item>
      <title>Ask the User Before You Act: Human-in-the-Loop Questions for AI Agents with Spring AI</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:17:44 +0000</pubDate>
      <link>https://dev.to/bytechef/ask-the-user-before-you-act-human-in-the-loop-questions-for-ai-agents-with-spring-ai-4k3d</link>
      <guid>https://dev.to/bytechef/ask-the-user-before-you-act-human-in-the-loop-questions-for-ai-agents-with-spring-ai-4k3d</guid>
      <description>&lt;p&gt;Every agent builder knows this failure mode. You give the agent a task, "plan our team offsite", and it just runs. It picks a city. It picks a budget. It picks the one week half the team is on holiday. Now you're not reviewing a plan. You're undoing one.&lt;/p&gt;

&lt;p&gt;The Spring AI team put it well in their &lt;a href="https://spring.io/blog/2026/01/16/spring-ai-ask-user-question-tool" rel="noopener noreferrer"&gt;AskUserQuestionTool post&lt;/a&gt;: the AI makes assumptions, and each assumption creates rework. Their fix is a tool that flips the default. The agent asks first, and acts only once it has real answers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/bytechefhq/bytechef" rel="noopener noreferrer"&gt;ByteChef&lt;/a&gt; is an open-source workflow automation and AI agent platform built on Spring AI, so it runs the same tool. In &lt;a href="https://blog.bytechef.io/blogs/bytechef-agent-skills" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; we gave agents know-how with skills. In this part we teach them to ask.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; The Ask User Question Tool lets a ByteChef AI Agent stop and ask the user 1 to 4 multiple-choice questions before it acts. In a deployed chat, the workflow suspends for up to 30 days and resumes when the answers arrive. No thread is held while it waits. Try it with the &lt;a href="https://www.bytechef.io/workflow-templates/ask-user-offsite-planner" rel="noopener noreferrer"&gt;offsite planner template&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The example: an offsite planner that asks first
&lt;/h2&gt;

&lt;p&gt;The template has three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New Chat Request&lt;/strong&gt; - a message arrives in the workflow chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Agent&lt;/strong&gt; - the Chat (stream) action with an Anthropic model, Chat Memory, and the Ask User Question Tool in its Tools slot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reply&lt;/strong&gt; - the plan goes back to the chat.&lt;/li&gt;
&lt;/ol&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%2Ftw14z5aktjyhqr3t8s9n.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%2Ftw14z5aktjyhqr3t8s9n.png" alt="The offsite planner template in the ByteChef editor: New Chat Request, AI Agent with the Ask User Question Tool, and Reply" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The system prompt does the real work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You plan team events. Never guess requirements. Before you plan anything,
call the AskUserQuestionTool once with three or four multiple-choice
questions covering budget, dates, team size and location. Offer 3 or 4
concrete options per question. After the answers arrive, produce a
one-page plan.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Never guess requirements"&lt;/strong&gt; tells the model that asking is expected, not a failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Call the tool once"&lt;/strong&gt; stops it from asking one question at a time and dragging the conversation out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the model actually sees
&lt;/h2&gt;

&lt;p&gt;The tool comes from Spring AI's &lt;code&gt;spring-ai-agent-utils&lt;/code&gt; toolkit. The model sees a tool named &lt;code&gt;AskUserQuestionTool&lt;/code&gt; that takes 1 to 4 questions. Each question has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;header&lt;/code&gt; - a short label, 12 characters max, like "Budget"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;question&lt;/code&gt; - the question itself&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;options&lt;/code&gt; - 2 to 4 choices, each with a &lt;code&gt;label&lt;/code&gt; and a &lt;code&gt;description&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;multiSelect&lt;/code&gt; - for questions where more than one answer fits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the offsite planner, a call looks roughly like this:&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;"questions"&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;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Budget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What's the total budget for the offsite?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"multiSelect"&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;"options"&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;"Under $5k"&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;"Local venue, day trips"&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;"$5k to $15k"&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;"Nearby city, one night"&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;"$15k+"&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;"Travel, multiple nights"&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;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which week works best?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"multiSelect"&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;"options"&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;"Mid May"&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;"Before summer holidays"&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;"Early June"&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;"Good weather, busier venues"&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;"September"&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;"After holidays, everyone back"&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;The model writes these itself, from the user's request and your system prompt.&lt;/p&gt;

&lt;p&gt;In plain Spring AI you'd also write a &lt;strong&gt;question handler&lt;/strong&gt;: the code that shows the questions and collects the answers. On a console, that's reading a line from stdin. In a real product, the user might answer in ten seconds or tomorrow, from a different device. That gap is what ByteChef fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when the agent asks
&lt;/h2&gt;

&lt;p&gt;When the agent calls the tool, ByteChef sends the questions to the chat as an &lt;code&gt;ask_user_question&lt;/code&gt; event. The chat renders each question with its header and a numbered list of options. You answer by typing, so you can pick an option or write your own answer.&lt;/p&gt;

&lt;p&gt;Here's a real test run. Asked to plan a two-day offsite, the agent sent four questions instead of a plan:&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%2F2uwshd5mxqexfxfyanef.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%2F2uwshd5mxqexfxfyanef.png" alt="The agent asking four multiple-choice questions in the ByteChef chat before planning the offsite" width="800" height="750"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What happens next depends on where the chat runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In a deployed workflow&lt;/strong&gt;, the questions come with a resume URL and the workflow suspends. It isn't holding a thread. The run is persisted and can wait up to 30 days. When you answer, the chat posts to the resume URL, and the AI Agent picks up exactly where it stopped, with your answers as the tool result, and streams the plan back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the editor's test chat&lt;/strong&gt;, there's no resume URL. The questions still show, but the agent is told to end its turn and read the answers from your next message. Because the template has Chat Memory, the agent still sees its own questions when your answer arrives, so it behaves the same.&lt;/p&gt;

&lt;p&gt;This suspend/resume mechanism is the same one behind ByteChef's &lt;a href="https://blog.bytechef.io/blogs/approval-human-in-the-loop" rel="noopener noreferrer"&gt;Approval component&lt;/a&gt;. Waiting for someone to approve and waiting for someone to answer are the same problem: a run that has to stop, wait for days, survive a restart, and continue on an outside signal.&lt;/p&gt;

&lt;p&gt;Two limits worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The tool needs the Chat (stream) action.&lt;/strong&gt; With the non-streaming Chat action there's no way to deliver the questions, so the agent is told to proceed on its best judgment and state its assumptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the questions can't be delivered, the agent gets the same instruction.&lt;/strong&gt; It never waits for answers nobody can see.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it in 4 steps
&lt;/h2&gt;

&lt;p&gt;The template needs only an Anthropic connection.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Import the template.&lt;/strong&gt; Open &lt;a href="https://www.bytechef.io/workflow-templates/ask-user-offsite-planner" rel="noopener noreferrer"&gt;Plan a team offsite with an agent that asks before it plans&lt;/a&gt; and add it to a project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect the model.&lt;/strong&gt; Add your Anthropic connection to the model under the AI Agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test it in the editor.&lt;/strong&gt; Open the chat and write "Plan an offsite for my team." You should get 3 or 4 questions back instead of a plan. Reply with your choices in one message and the agent writes the plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy it.&lt;/strong&gt; Deploy the project, open &lt;strong&gt;Chats&lt;/strong&gt; in the Automation sidebar, and pick the workflow. Now the run really pauses while it waits for you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks to Chat Memory, you can ask for changes afterwards, like "move it to September", without starting over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why asking first matters
&lt;/h2&gt;

&lt;p&gt;It's tempting to treat clarifying questions as polite extras. For agents, they're closer to a correctness tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They prevent rework.&lt;/strong&gt; A wrong guess costs the failed step, the tokens spent getting there, and the turns you spend correcting it. One round of questions is cheaper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They're a natural human-in-the-loop gate.&lt;/strong&gt; For anything that matters, like spending money, emailing a customer, or deleting records, "ask before acting" is the safe default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They don't tie anything up.&lt;/strong&gt; A deployed run actually suspends. The agent can wait days without holding a worker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Spring AI vs. ByteChef: what you get for free
&lt;/h2&gt;

&lt;p&gt;The Spring AI post ends where a library ends: here's the tool, write a handler. Here's what ByteChef adds on top:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plain Spring AI&lt;/th&gt;
&lt;th&gt;ByteChef&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You implement the question handler&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A console handler blocks the current thread&lt;/td&gt;
&lt;td&gt;Deployed workflow suspends up to 30 days and frees the worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Getting questions to a UI is your job&lt;/td&gt;
&lt;td&gt;Streamed to the chat, run resumes when the answer is posted back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding the tool means code and config&lt;/td&gt;
&lt;td&gt;One tool on the AI Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The question format, the library, and model portability stay the same. The template uses Claude, but the tool works with any model provider ByteChef supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The most useful agent behaviors are often the boring ones. "Ask before you assume" isn't flashy, but it's the difference between an agent whose work you undo and one whose work you can trust.&lt;/p&gt;

&lt;p&gt;Import the &lt;a href="https://www.bytechef.io/workflow-templates/ask-user-offsite-planner" rel="noopener noreferrer"&gt;offsite planner template&lt;/a&gt;, add your Anthropic connection, and ask it to plan your next offsite. Then add the Ask User Question Tool to your own agents wherever a guess would be expensive.&lt;/p&gt;

&lt;p&gt;If you like where this is going, a star on &lt;a href="https://github.com/bytechefhq/bytechef" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps a lot.&lt;/p&gt;

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

</description>
      <category>ai</category>
      <category>java</category>
      <category>opensource</category>
      <category>agents</category>
    </item>
    <item>
      <title>Turn One Sentence Into a Working Automation: Prompt-to-Workflow in Your SaaS</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Fri, 30 Oct 2026 12:51:44 +0000</pubDate>
      <link>https://dev.to/bytechef/turn-one-sentence-into-a-working-automation-prompt-to-workflow-in-your-saas-403k</link>
      <guid>https://dev.to/bytechef/turn-one-sentence-into-a-working-automation-prompt-to-workflow-in-your-saas-403k</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.bytechef.io/blogs/embedded-prompt" rel="noopener noreferrer"&gt;ByteChef blog&lt;/a&gt;. Part 10 of the ByteChef Embedded series.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Some automation requests are fuzzy. Those deserve a conversation. But a lot of them aren't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"When a lead comes in, post it to #sales and email them a welcome note."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's already a full spec. A chat would only slow the user down.&lt;/p&gt;

&lt;p&gt;So we built &lt;strong&gt;New from Prompt&lt;/strong&gt;: the user writes a sentence, clicks Generate, and gets a finished, validated workflow back. Your app makes one HTTP call. No streaming, no transcript, no client-side AI code.&lt;/p&gt;

&lt;p&gt;Here's how it works and what it takes to ship it in your own product.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One request: &lt;code&gt;POST /api/embedded/v1/automation/workflows/generate&lt;/code&gt; with &lt;code&gt;{prompt, systemPrompt?}&lt;/code&gt;, signed with the connected user's JWT.&lt;/li&gt;
&lt;li&gt;ByteChef creates a draft workflow in the user's own project, runs its workflow agent in &lt;strong&gt;autonomous mode&lt;/strong&gt;, and returns the new workflow's UUID once it's built and validated.&lt;/li&gt;
&lt;li&gt;The agent only uses integrations you enabled plus connection-free components (webhooks, schedules, conditions, loops).&lt;/li&gt;
&lt;li&gt;The result is a draft. Nothing runs until the user reviews and publishes it.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;ByteChef Embedded gives your users five ways to create 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%2F7paczagpr3iw7x48f1uf.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%2F7paczagpr3iw7x48f1uf.png" alt="Create menu in the sample app" width="798" height="161"&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&lt;/td&gt;
&lt;td&gt;Picks a workflow you built&lt;/td&gt;
&lt;td&gt;You know the common automations up front&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 is comfortable with visual builders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New from Prompt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Writes one description, gets one workflow&lt;/td&gt;
&lt;td&gt;The request is short and clear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New from Chat&lt;/td&gt;
&lt;td&gt;Talks it through with an agent&lt;/td&gt;
&lt;td&gt;The request is fuzzy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New from Custom Workflow Builder&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;Prompt is the lowest-effort AI option, for the user and for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dialog
&lt;/h2&gt;

&lt;p&gt;Two fields: what the user wants, and an optional system prompt with your house rules.&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%2Fkdn8mo6f0n2qbidzbu9u.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%2Fkdn8mo6f0n2qbidzbu9u.png" alt="Empty Generate Workflow dialog" width="560" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The form rejects anything under ten characters, so "Slack" alone doesn't hit the server:&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%2Fueeen3fjid82m1v5rnr1.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%2Fueeen3fjid82m1v5rnr1.png" alt="Validation error" width="560" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While ByteChef works, the button reads "Generating...". When the UUID comes back, the dialog closes and the list reloads. Our lead workflow took 47 seconds.&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%2Fqklep4thscmrhsolqsre.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%2Fqklep4thscmrhsolqsre.png" alt="Dialog while generating" width="560" height="440"&gt;&lt;/a&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%2Fp268suixq3ctdmqw41a2.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%2Fp268suixq3ctdmqw41a2.png" alt="Workflow list after generation" width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The component is a plain &lt;code&gt;react-hook-form&lt;/code&gt; dialog:&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="nx"&gt;formSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please describe the workflow in a sentence or two.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&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;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;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormValues&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setIsSubmitting&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="k"&gt;try&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;generateWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&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;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;onWorkflowGenerated&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&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="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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;Failed to generate workflow. Please try again.&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;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setIsSubmitting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;All the AI lives on the server. The browser collects two strings and waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  One call
&lt;/h2&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;generateWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&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;systemPrompt&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;trimmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&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="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/workflows/generate&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;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;trimmed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trimmed&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;prompt&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 generate workflow: &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;uuid&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;uuid&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;uuid&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;uuid&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;uuid&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 wire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/embedded/v1/automation/workflows/generate
Authorization: Bearer &amp;lt;connected user's JWT&amp;gt;
X-Environment: DEVELOPMENT
Content-Type: application/json

{
  "prompt": "When a new lead arrives on a webhook with name, email and company, post it to the #sales Slack channel and send the lead a short welcome email from Gmail.",
  "systemPrompt": "Always notify the team in Slack, never by email. Keep workflows under five steps."
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="s2"&gt;"8b0c5d1e-..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's synchronous.&lt;/strong&gt; ByteChef answers when the agent is done. Small workflows take under a minute, big ones several. The server waits up to ten minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send &lt;code&gt;X-Environment&lt;/code&gt;.&lt;/strong&gt; It defaults to &lt;code&gt;PRODUCTION&lt;/code&gt;, which is rarely what you want while developing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Four shapes of the same endpoint
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Called from&lt;/th&gt;
&lt;th&gt;Create new&lt;/th&gt;
&lt;th&gt;Rebuild existing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser (user's JWT)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /automation/workflows/generate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /automation/workflows/{uuid}/generate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your backend (API key)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /{externalUserId}/automation/workflows/generate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /{externalUserId}/automation/workflows/{uuid}/generate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rebuild form powers a "Change it with AI" button: same UUID, rewritten definition. The backend form lets your server generate a starter automation right after onboarding, so the user's Automations page isn't empty on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens on the server
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resolve user and project.&lt;/strong&gt; ByteChef reads the external user ID from the JWT and finds (or creates) their personal project for that environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a draft.&lt;/strong&gt; An empty workflow is saved first, labeled with the user's prompt (cut to 80 chars).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the allow-list.&lt;/strong&gt; Every component with an enabled integration in this environment, plus every connection-free component.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the agent autonomously.&lt;/strong&gt; The same BUILD agent behind New from Chat and ByteChef's Copilot, with &lt;code&gt;autonomous: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return the UUID.&lt;/strong&gt; On failure or timeout, the draft is deleted. A failed generation leaves nothing behind.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Autonomous mode
&lt;/h2&gt;

&lt;p&gt;In autonomous mode the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;never asks follow-up questions; it picks the most common, sensible default&lt;/li&gt;
&lt;li&gt;never pauses to show a plan&lt;/li&gt;
&lt;li&gt;keeps the label it was given&lt;/li&gt;
&lt;li&gt;saves once, at the end, after the full workflow validates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise it's the same agent: it searches the catalog (&lt;code&gt;searchTask&lt;/code&gt;, &lt;code&gt;listTasks&lt;/code&gt;), reads definitions (&lt;code&gt;getTaskDefinition&lt;/code&gt;, &lt;code&gt;getTaskProperties&lt;/code&gt;), validates each task (&lt;code&gt;validateTask&lt;/code&gt;), then validates the whole workflow before saving. What you get back isn't a guess at JSON. It passed the same validator as a hand-built workflow.&lt;/p&gt;

&lt;p&gt;Where the chat agent would stop and show a channel picker or a "Connect Slack" card, the prompt agent picks a value, builds the step, and leaves the connection for the user to choose in the editor.&lt;/p&gt;

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Always notify the team in Slack, never by email.
Keep workflows under five steps.
Use the Europe/Berlin time zone for every schedule unless the user names another.
Never delete records in the CRM; update a status field instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note how this plays with our example. The user asked for a welcome email to the lead; the rule says "never by email". The agent read the rule the way a colleague would: it's about notifying the team. It built three steps: webhook trigger, Slack message to #sales, Gmail welcome email to the lead.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Trimmed and capped at 4,000 characters.&lt;/li&gt;
&lt;li&gt;Applies to one generation only. Nothing is stored, so send it every time.&lt;/li&gt;
&lt;li&gt;Appended under a heading marking it as integrator instructions, applied only where it doesn't conflict with build rules, the workflow contract or safety constraints. It steers the agent; it can't hijack it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the sample it's a text box so you can play with it. In production, generate it in your code from your conventions and what you know about the user. Don't hand it to end users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security boundaries
&lt;/h2&gt;

&lt;p&gt;Anyone with a connected user's JWT can call this with any text. So ByteChef trusts nothing in the request except the two strings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The workflow is always the user's.&lt;/strong&gt; Rebuild looks the UUID up inside the user's project only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent state is built server-side.&lt;/strong&gt; Mode, workflow ID, project and environment don't come from the request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent stays in the user's project.&lt;/strong&gt; No touching other workflows, no project-level changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The allow-list is enforced everywhere.&lt;/strong&gt; Components outside it don't show up in search, can't be looked up, and are rejected on validation, including inside conditions, loops and AI agent nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters more here than in chat. A chat agent asked for "Salesforce deals to Slack" when you don't offer Salesforce can ask what to do instead. A prompt agent can't ask, so it never produces nodes for apps your users can't connect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing off to the editor
&lt;/h2&gt;

&lt;p&gt;The generated workflow is a real ByteChef workflow, so send the user straight to the 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="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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GenerateWorkflowDialog&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="na"&gt;onWorkflowGenerated&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;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="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 what the agent built from the lead prompt:&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%2F8juw2cfj2cfechqx103p.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%2F8juw2cfj2cfechqx103p.png" alt="Generated workflow in the editor" width="799" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It set the Slack channel to &lt;code&gt;sales&lt;/code&gt;, wrote a message referencing the webhook payload (&lt;code&gt;${autoRespondWithHTTP200_1.body.name}&lt;/code&gt; and friends), addressed the Gmail step to the lead, and even added a workflow description.&lt;/p&gt;

&lt;p&gt;The editor shows what's left:&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%2F1tact8astz6vmrmd5o9f.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%2F1tact8astz6vmrmd5o9f.png" alt="Editor issues panel" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2 errors:&lt;/strong&gt; the Slack and Gmail connections. The agent can't pick accounts for the user. With &lt;code&gt;connectionDialogAllowed={true}&lt;/code&gt;, they connect right there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 warnings:&lt;/strong&gt; webhook fields. A webhook payload has no fixed shape, so the editor reminds you to check &lt;code&gt;body.name&lt;/code&gt;, &lt;code&gt;body.email&lt;/code&gt; and &lt;code&gt;body.company&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Draft to running
&lt;/h2&gt;

&lt;p&gt;Generated workflows are drafts. Nothing runs until published and enabled, which is the right default for something an AI just wrote:&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;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;Generated from prompt&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;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;h2&gt;
  
  
  Prompt vs. Chat
&lt;/h2&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;New from Prompt&lt;/th&gt;
&lt;th&gt;New from Chat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requests&lt;/td&gt;
&lt;td&gt;One POST, one JSON response&lt;/td&gt;
&lt;td&gt;One AG-UI stream per turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent mode&lt;/td&gt;
&lt;td&gt;Autonomous, picks defaults&lt;/td&gt;
&lt;td&gt;Interactive, asks questions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing connection&lt;/td&gt;
&lt;td&gt;Builds the step, user connects in editor&lt;/td&gt;
&lt;td&gt;Shows a Connect card in the thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code in your app&lt;/td&gt;
&lt;td&gt;A form and a fetch&lt;/td&gt;
&lt;td&gt;A chat component and its deps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Clear, short requests&lt;/td&gt;
&lt;td&gt;Fuzzy requests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They combine well: generate a first version from a prompt, then offer "Change it with AI" via the rebuild endpoint.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bytechef.ai.copilot.enabled=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;An AI provider configured for the environment&lt;/li&gt;
&lt;li&gt;Integrations enabled for the environment (without them, the agent can still build schedules, webhooks and logic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Embedded workflows and workflow generation are Enterprise Edition features.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raise timeouts&lt;/strong&gt; on every proxy, load balancer and serverless function in the path (many stop at 30 or 60 seconds), or call the backend endpoint from a job and notify the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show real progress.&lt;/strong&gt; "Generating..." is fine for ten seconds, not two minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map errors&lt;/strong&gt; to messages users can act on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect to the editor&lt;/strong&gt; with the returned UUID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own the system prompt&lt;/strong&gt; in code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send &lt;code&gt;X-Environment&lt;/code&gt;&lt;/strong&gt; explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate-limit generation.&lt;/strong&gt; Every request burns model tokens on your AI provider account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seed better examples.&lt;/strong&gt; Use your own integrations in the placeholder, or offer clickable starter prompts.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt; the button, the form, the system prompt, which integrations are enabled, the waiting experience, what happens with the UUID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ByteChef:&lt;/strong&gt; the per-user project and draft, the agent and its tools, catalog search and validation, the allow-list and security boundary, the editor, ConnectDialog and credential storage, and the engine that runs the result.&lt;/p&gt;

&lt;p&gt;Your code is a form, one fetch and a redirect. Behind it is an agent that knows every action in your catalog.&lt;/p&gt;

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

&lt;p&gt;Enable the copilot and an AI provider in ByteChef, run the &lt;a href="https://github.com/bytechefhq/bytechef" rel="noopener noreferrer"&gt;embedded sample app&lt;/a&gt;, and open &lt;strong&gt;Automations → New from Prompt&lt;/strong&gt;. Describe something small with one of your connected apps and click Generate.&lt;/p&gt;

&lt;p&gt;ByteChef is open source (Apache 2.0). If this was useful, a ⭐ on &lt;a href="https://github.com/bytechefhq/bytechef" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps a lot.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The full series: connect apps, ComponentKit, AI chat assistant, event triggers, workflows as APIs, MCP agents, custom builder, templates, New from Chat, and New from Prompt. All on the &lt;a href="https://blog.bytechef.io" rel="noopener noreferrer"&gt;ByteChef blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>automation</category>
      <category>saas</category>
    </item>
    <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>
  </channel>
</rss>
