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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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




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

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

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

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

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




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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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




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

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

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

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

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




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

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

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

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

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




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

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

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

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

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

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

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




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

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

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




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

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

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

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

</description>
    </item>
    <item>
      <title>Your Documents, Chunked and Searchable: The Knowledge Base in ByteChef</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Tue, 25 Aug 2026 19:46:33 +0000</pubDate>
      <link>https://dev.to/bytechef/your-documents-chunked-and-searchable-the-knowledge-base-in-bytechef-h9d</link>
      <guid>https://dev.to/bytechef/your-documents-chunked-and-searchable-the-knowledge-base-in-bytechef-h9d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A &lt;strong&gt;Knowledge Base&lt;/strong&gt; in ByteChef is a managed RAG store: drop in documents (PDF, Markdown, Word, JSON, plain text - and scanned PDFs or images, through an &lt;strong&gt;OCR&lt;/strong&gt; path) and ByteChef parses, &lt;strong&gt;chunks&lt;/strong&gt; (with size and overlap you pick per knowledge base), &lt;strong&gt;embeds&lt;/strong&gt;, and indexes them in a &lt;strong&gt;pgvector&lt;/strong&gt;-backed Spring AI &lt;code&gt;VectorStore&lt;/code&gt;. Retrieval is everywhere you'd want it: a &lt;strong&gt;Knowledge Base Search tool&lt;/strong&gt; for AI Agents (with tag filtering), &lt;strong&gt;Load/Search/Update/Delete actions&lt;/strong&gt; for workflows, and a built-in &lt;strong&gt;search interface&lt;/strong&gt; for testing. Keeping the index current is yours to drive - re-run an ingestion workflow on whatever schedule you like - and the part to plan for deliberately is upstream &lt;strong&gt;deletes&lt;/strong&gt;, which no refresh can spot on your behalf.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every team building with agents arrives at the same sentence: &lt;em&gt;"It should know our stuff."&lt;/em&gt; The product docs. The runbooks. The policy PDFs. And the standard answer - RAG, retrieval-augmented generation - is conceptually simple: chunk the documents, embed the chunks, search by similarity, hand the best matches to the model.&lt;/p&gt;

&lt;p&gt;The concept is a weekend. The &lt;em&gt;operation&lt;/em&gt; is a project. Someone has to parse five file formats, pick chunk sizes, run an embedding model, host a vector database, keep the index in sync when the source-of-truth changes - and notice when a document is &lt;em&gt;deleted&lt;/em&gt; upstream, because a knowledge base that confidently serves stale policy is worse than none at all. That's the gap between a RAG demo and a RAG system. ByteChef's &lt;strong&gt;Knowledge Base&lt;/strong&gt; closes most of it for you - and this post is honest about the part it leaves to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Knowledge Base Is
&lt;/h2&gt;

&lt;p&gt;In ByteChef, a Knowledge Base is a first-class object with its own home in the workspace: a named store of &lt;strong&gt;documents&lt;/strong&gt;, each broken into &lt;strong&gt;chunks&lt;/strong&gt;, each chunk embedded and indexed for semantic search. The layering is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documents&lt;/strong&gt; are what you put in - files you upload, or records a workflow or an agent writes in, organizable with &lt;strong&gt;tags&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunks&lt;/strong&gt; are what retrieval actually works with. You pick the chunking when you create a knowledge base - maximum chunk size (default 1024 tokens), the minimum chunk size in characters used to find a clean break point (default 100), and how many tokens neighboring chunks &lt;strong&gt;overlap&lt;/strong&gt; (default 200) - so a KB full of dense legal prose can chunk differently than one full of short FAQ entries. The splitter counts tokens with &lt;code&gt;CL100K_BASE&lt;/code&gt;, the same encoding GPT-4 uses, and prefers to break on sentence punctuation rather than mid-thought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt; live in &lt;strong&gt;pgvector&lt;/strong&gt; - a Spring AI &lt;code&gt;PgVectorStore&lt;/code&gt; over a pgvector-enabled PostgreSQL database, built with the platform's configured &lt;code&gt;EmbeddingModel&lt;/code&gt; and indexed with HNSW over cosine distance. There's no proprietary vector service to sign up for and no new operational vocabulary to learn: it's Postgres, backed up and monitored like the rest of your Postgres, and ByteChef creates and maintains the schema, the index, and the per-knowledge-base partitioning for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And in the Knowledge Base workspace, none of this is a black box. You can open any document and &lt;strong&gt;inspect its individual chunks&lt;/strong&gt; - and &lt;em&gt;edit&lt;/em&gt; them, because sometimes the fix for a bad retrieval is one badly split paragraph - watch a document's indexing status, and, before any agent ever touches it, try queries against the KB in a built-in &lt;strong&gt;search interface&lt;/strong&gt; to see exactly what retrieval returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Documents In
&lt;/h2&gt;

&lt;p&gt;There are three doors in, matched to three situations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload.&lt;/strong&gt; Drag files into the workspace. The ingestion pipeline picks the right parser per format - a dedicated PDF reader (page- or paragraph-oriented), a Markdown reader, JSON and plain-text readers, and an &lt;a href="https://tika.apache.org/" rel="noopener noreferrer"&gt;Apache Tika&lt;/a&gt;-based reader as the catch-all for Word documents and other office formats. All of these are Spring AI's document readers; ByteChef orchestrates them into one pipeline that ends in chunks and vectors. With OCR enabled, PDFs and images take a different route entirely - straight through an OCR service - so the scanned contract and the photographed whiteboard land in the index like everything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From a workflow.&lt;/strong&gt; The &lt;strong&gt;Load Data&lt;/strong&gt; action writes into a knowledge base from anywhere a workflow can reach. Attach a &lt;strong&gt;document reader&lt;/strong&gt; (plus any transformers you want), point the action at the target KB, and every run pushes documents through the same chunk-and-embed pipeline the uploader uses. Put that workflow behind a schedule, a webhook, or a file-landed trigger and ingestion runs without you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From an agent.&lt;/strong&gt; The &lt;strong&gt;Knowledge Base Update&lt;/strong&gt; tool is a cluster element you attach to an AI Agent, so an agent can contribute what it learns rather than only consume it - useful when the thing worth keeping is something the agent worked out mid-conversation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Whichever door you use, everything downstream is identical - parse, chunk, embed, index. What differs is who pushes the button, and how often.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping It Current - and the Deletes Nobody Plans For
&lt;/h2&gt;

&lt;p&gt;An index is a snapshot, and snapshots rot. Re-running ingestion is the easy half: put the &lt;strong&gt;Load Data&lt;/strong&gt; workflow on a schedule and new and changed documents flow in on whatever cadence suits the material - nightly for a handbook, hourly for a status page.&lt;/p&gt;

&lt;p&gt;The hard half is the one that quietly poisons RAG systems: &lt;strong&gt;deletions&lt;/strong&gt;. A refresh that asks the upstream for "everything that changed since Tuesday" gets back the records that still exist - never the ones that vanished. Left alone, a document deleted at the source lives on in your knowledge base indefinitely, still retrieved, still quoted by your agent, long after somebody retracted it.&lt;/p&gt;

&lt;p&gt;That half is yours to own today - ByteChef has no "connect a source and forget it" option that derives deletions for you. It's worth owning deliberately rather than discovering later, and the shape of the fix is simple enough: tag each ingested document with its upstream id, and on the runs where you can enumerate the full current upstream set, use the &lt;strong&gt;Delete Documents&lt;/strong&gt; action - it removes documents by metadata filter - to drop whatever is no longer in it. Unglamorous, and far cheaper than the alternative, which is an agent confidently citing a policy that was withdrawn a month ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Knowledge Out
&lt;/h2&gt;

&lt;p&gt;A knowledge base earns its keep at retrieval time, and ByteChef exposes it on every surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As an AI Agent tool.&lt;/strong&gt; The &lt;strong&gt;Knowledge Base Search&lt;/strong&gt; tool is a cluster element - attach it to any AI Agent (the same way as every tool in our &lt;a href="https://blog.bytechef.io/blogs/bytechef-agentic-patterns" rel="noopener noreferrer"&gt;agentic patterns series&lt;/a&gt;), pick which KB it searches, and optionally scope it by &lt;strong&gt;tags&lt;/strong&gt; (documents matching &lt;em&gt;any&lt;/em&gt; selected tag are eligible). From then on, the agent decides when to reach into the knowledge base, mid-conversation, on its own. There's a &lt;strong&gt;Knowledge Base Update&lt;/strong&gt; tool too - for agents that should be able to &lt;em&gt;contribute&lt;/em&gt; knowledge, not just consume it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As workflow actions.&lt;/strong&gt; The Knowledge Base component ships &lt;strong&gt;Load Data&lt;/strong&gt;, &lt;strong&gt;Search Data&lt;/strong&gt;, &lt;strong&gt;Update Documents&lt;/strong&gt;, and &lt;strong&gt;Delete Documents&lt;/strong&gt; actions, so ordinary workflows can query or maintain a KB with no agent involved - "on new support ticket, search the KB and attach the top three matches" is three nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One store, several ways in and out - and because retrieval is tag-filterable, a single knowledge base can serve multiple audiences ("customer-facing" vs. "internal-only") without maintaining parallel copies.&lt;/p&gt;

&lt;p&gt;If you want to tune retrieval &lt;em&gt;itself&lt;/em&gt; - query rewriting, expansion, custom joins over several stores - that's a different layer: ByteChef's RAG cluster elements, which work over any vector store you like. The Knowledge Base is the batteries-included end of the same spectrum: you don't configure retrieval, you just ask it questions.&lt;/p&gt;

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

&lt;p&gt;By now you know the shape of this section. The primitives are Spring AI's; the product around them is ByteChef's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Document readers exist as library classes (PDF, Markdown, JSON, text, Tika)&lt;/em&gt; → ByteChef &lt;strong&gt;auto-selects the parser per file&lt;/strong&gt; in one managed ingestion pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Spring AI has no OCR reader at all&lt;/em&gt; → ByteChef adds one, and &lt;strong&gt;routes scanned PDFs and images through it&lt;/strong&gt; before any other parser gets a look.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;&lt;code&gt;PgVectorStore&lt;/code&gt; + &lt;code&gt;EmbeddingModel&lt;/code&gt; are yours to configure and operate&lt;/em&gt; → ByteChef &lt;strong&gt;owns the schema, the HNSW index, and the per-knowledge-base partitioning&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Chunking is a splitter you instantiate with magic numbers&lt;/em&gt; → chunk size, minimum, and overlap are &lt;strong&gt;fields on the create-knowledge-base form&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Ingestion is a pipeline you assemble, wire, and run yourself&lt;/em&gt; → it's &lt;strong&gt;one action with a reader attached&lt;/strong&gt;, on any trigger - or no workflow at all, if you just upload.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Retrieval is code you write&lt;/em&gt; → it's a &lt;strong&gt;tool on any agent&lt;/strong&gt;, &lt;strong&gt;actions in any workflow&lt;/strong&gt;, and a &lt;strong&gt;search UI&lt;/strong&gt; for humans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same foundation as everything in this series - Spring AI under the hood - with the operational half of RAG, the half that actually hurts, absorbed into the platform.&lt;/p&gt;

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

&lt;p&gt;If you're self-hosting, the Knowledge Base ships &lt;strong&gt;disabled&lt;/strong&gt; - two switches turn it on:&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;bytechef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;knowledge-base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;vectorstore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgvector&lt;/span&gt;
      &lt;span class="na"&gt;pgvector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdbc:postgresql://localhost:5433/bytechef_vectorstore&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second block is the part worth planning for. The vector index lives in its &lt;strong&gt;own&lt;/strong&gt; pgvector-enabled PostgreSQL database, separate from ByteChef's application database - the development compose file runs one for you alongside the main Postgres, and in production it's a second database to provision, monitor, and back up. Still Postgres, still nothing exotic, but not the &lt;em&gt;same&lt;/em&gt; Postgres. ByteChef creates the schema and the index inside it on first use.&lt;/p&gt;

&lt;p&gt;One more prerequisite, easy to miss: an &lt;strong&gt;embedding model&lt;/strong&gt; has to be active for the environment. Without one there's nothing to turn chunks into vectors with, and the Knowledge Base page says so outright rather than quietly indexing nothing.&lt;/p&gt;

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

&lt;p&gt;The distance between "agents are impressive" and "agents are useful &lt;em&gt;here&lt;/em&gt;" is almost always knowledge - your documents, your data, your vocabulary. RAG closes that distance in principle; in practice it comes bundled with a parsing zoo, a vector database, an embedding budget, sync jobs, and the deletion problem nobody budgets for.&lt;/p&gt;

&lt;p&gt;ByteChef's Knowledge Base packages most of it: documents in through upload, a workflow action, or an agent; chunks and vectors in pgvector; retrieval exposed as agent tools, workflow actions, and a human search box. What stays with you is the refresh policy - how often to re-run ingestion, and what to do about the deletes. Your agents get something to know, and you skip almost all of the machinery of knowing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have a folder of PDFs your agent should be quoting? Open &lt;a href="https://app.bytechef.io" rel="noopener noreferrer"&gt;ByteChef&lt;/a&gt;, create a **Knowledge Base&lt;/em&gt;&lt;em&gt;, drop them in, and attach the **Knowledge Base Search&lt;/em&gt;* tool to your agent.*&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>rag</category>
      <category>search</category>
    </item>
    <item>
      <title>Agentic Workflow Patterns in ByteChef: The Five Patterns, No Code Required</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Fri, 21 Aug 2026 21:21:45 +0000</pubDate>
      <link>https://dev.to/bytechef/agentic-workflow-patterns-in-bytechef-the-five-patterns-no-code-required-k2e</link>
      <guid>https://dev.to/bytechef/agentic-workflow-patterns-in-bytechef-the-five-patterns-no-code-required-k2e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Anthropic's &lt;a href="https://www.anthropic.com/research/building-effective-agents" rel="noopener noreferrer"&gt;Building Effective Agents&lt;/a&gt; research identified five workflow patterns behind most successful LLM systems: &lt;strong&gt;chain&lt;/strong&gt;, &lt;strong&gt;parallelization&lt;/strong&gt;, &lt;strong&gt;routing&lt;/strong&gt;, &lt;strong&gt;orchestrator-workers&lt;/strong&gt;, and &lt;strong&gt;evaluator-optimizer&lt;/strong&gt;. The Spring AI team &lt;a href="https://spring.io/blog/2025/01/21/spring-ai-agentic-patterns" rel="noopener noreferrer"&gt;implemented them in Java&lt;/a&gt;. ByteChef - which uses Spring AI under the hood - lets you build every one of them &lt;strong&gt;visually, on a workflow canvas, without writing code&lt;/strong&gt;. This post shows you how, pattern by pattern.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In late 2024, Anthropic published &lt;a href="https://www.anthropic.com/research/building-effective-agents" rel="noopener noreferrer"&gt;Building Effective Agents&lt;/a&gt;, a piece of research that has aged remarkably well. Its central claim: the most successful LLM systems in production aren't built on complex frameworks - they're built from &lt;strong&gt;simple, composable patterns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://spring.io/projects/spring-ai" rel="noopener noreferrer"&gt;Spring AI&lt;/a&gt; team followed up with an excellent &lt;a href="https://spring.io/blog/2025/01/21/spring-ai-agentic-patterns" rel="noopener noreferrer"&gt;blog post&lt;/a&gt; implementing those patterns in plain Java. If you're a Java developer, it's a great read.&lt;/p&gt;

&lt;p&gt;But here's the thing: ByteChef is built &lt;strong&gt;on top of Spring AI&lt;/strong&gt;. Every model integration, every structured-output call, every RAG pipeline in ByteChef runs on the same Spring AI foundation described in that post. Which means the same five agentic patterns are available to you - except instead of writing Java classes, you &lt;strong&gt;drag components onto a canvas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Anthropic draws a useful distinction between two kinds of agentic systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflows&lt;/strong&gt; - systems where LLMs and tools are orchestrated through predefined paths. &lt;em&gt;You&lt;/em&gt; decide the structure; the LLM fills in the intelligence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents&lt;/strong&gt; - systems where the LLM dynamically directs its own process and tool usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most production use cases are best served by workflows: they're predictable, debuggable, and cheaper to run. And a visual workflow builder is arguably the most natural way to express them - the pattern diagrams in Anthropic's paper practically &lt;em&gt;are&lt;/em&gt; ByteChef canvases.&lt;/p&gt;

&lt;p&gt;Let's build all five.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Building Blocks
&lt;/h2&gt;

&lt;p&gt;Before diving into the patterns, a quick inventory of the ByteChef pieces we'll compose them from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI model steps&lt;/strong&gt; - every AI provider component (OpenAI, Anthropic, Vertex Gemini, Mistral, Ollama, and &lt;a href="https://blog.bytechef.io/blogs/spring-ai-agent" rel="noopener noreferrer"&gt;many more&lt;/a&gt;) exposes an &lt;em&gt;Ask&lt;/em&gt; action: prompt in, response out. Crucially, you can set the &lt;strong&gt;Response Format&lt;/strong&gt; to &lt;em&gt;Structured data&lt;/em&gt; and define a &lt;strong&gt;Response Schema&lt;/strong&gt;, so the model returns typed JSON instead of free text - the visual equivalent of Spring AI's structured output converter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI Agent component&lt;/strong&gt; - ByteChef's full agent building block, with pluggable Model, RAG, Memory, Tools, and Guardrails cluster elements. We covered it in depth in &lt;a href="https://blog.bytechef.io/blogs/spring-ai-agent" rel="noopener noreferrer"&gt;Building Intelligent Automation with ByteChef and AI Agents&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow controls&lt;/strong&gt; - the visual control-flow primitives that give workflows the same expressive power as code: &lt;a href="https://blog.bytechef.io/blogs/guide-to-flow-controls" rel="noopener noreferrer"&gt;Condition, Branch, and Loop&lt;/a&gt; for choice and repetition, plus &lt;a href="https://blog.bytechef.io/blogs/flow-controls-complete" rel="noopener noreferrer"&gt;Parallel, Fork/Join, Each, and Map&lt;/a&gt; for fanning out. Loop Break, which ends a loop early from inside its body, pairs with Loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data pills&lt;/strong&gt; - ByteChef's way of wiring one step's output into another step's input. Wherever you see "pass the result to the next step" below, that's a drag-and-drop data pill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole toolbox. Now the patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Chain Workflow
&lt;/h2&gt;

&lt;p&gt;The simplest pattern, and the one to reach for first: decompose a task into sequential steps, where each LLM call processes the output of the previous one.&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%2Fztcbi0bgysewerx83g0e.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%2Fztcbi0bgysewerx83g0e.png" alt=" " width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why bother with three small prompts instead of one big one? Because each step gets a &lt;strong&gt;focused, simple instruction&lt;/strong&gt; - and focused instructions produce dramatically more reliable results than one prompt trying to do everything at once. You're trading a little latency for a lot of accuracy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building it in ByteChef
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add an AI model step (say, OpenAI &lt;em&gt;Ask&lt;/em&gt;) with a prompt that handles only the &lt;strong&gt;first&lt;/strong&gt; transformation - e.g. &lt;em&gt;"Extract every numerical value and its metric from this text."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Add a second model step whose prompt performs the next transformation, and reference the first step's output as a data pill: &lt;em&gt;"Convert all values to percentages: &lt;code&gt;${openai_1}&lt;/code&gt;"&lt;/em&gt;. (In text mode an &lt;em&gt;Ask&lt;/em&gt; step outputs its response string directly - field references like &lt;code&gt;${openai_1.category}&lt;/code&gt; become available once you switch it to &lt;em&gt;Structured data&lt;/em&gt;.)&lt;/li&gt;
&lt;li&gt;Keep chaining. Each step sees only what the previous step produced.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also insert a &lt;strong&gt;Condition&lt;/strong&gt; between steps as a &lt;em&gt;gate&lt;/em&gt; - Anthropic's recommended addition - to verify an intermediate result before continuing (for example, checking that step one actually produced a number before running the expensive formatting step).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; the task has clear sequential stages, each stage builds on the last, and you'd rather wait an extra second than get a sloppy answer. A typical example: &lt;em&gt;extract data → normalize it → sort it → format it as a Markdown table&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Parallelization Workflow
&lt;/h2&gt;

&lt;p&gt;Some tasks aren't sequential - they're several independent subtasks that can run &lt;strong&gt;at the same time&lt;/strong&gt;, with the results aggregated at the end.&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%2Fh132p2nsuuta4bjdea4f.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%2Fh132p2nsuuta4bjdea4f.png" alt=" " width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anthropic describes two flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sectioning&lt;/strong&gt; - split the task into independent subtasks (analyze the same document from a legal, financial, and technical perspective).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voting&lt;/strong&gt; - run the &lt;em&gt;same&lt;/em&gt; prompt several times and compare answers for consensus (useful for high-stakes classification).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building it in ByteChef
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add a &lt;strong&gt;Fork/Join&lt;/strong&gt; flow control. Each branch runs in parallel as an isolated sub-flow.&lt;/li&gt;
&lt;li&gt;In each branch, add an AI model step with its own perspective-specific prompt. All branches can reference the same input via data pills.&lt;/li&gt;
&lt;li&gt;After the join, add one final model step that receives all branch outputs and synthesizes them: &lt;em&gt;"Combine these three analyses into a single executive summary."&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For list-shaped work - "run this same LLM analysis over 50 support tickets" - use the &lt;strong&gt;Each&lt;/strong&gt; flow control instead: it iterates over the items in parallel, applying the same steps to every one. And with the &lt;strong&gt;Parallel&lt;/strong&gt; flow control you can fire off a set of independent tasks without waiting for each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; subtasks are genuinely independent, you need multiple perspectives on the same input, or you're processing volumes where sequential execution would be painfully slow.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Routing Workflow
&lt;/h2&gt;

&lt;p&gt;Routing classifies the input first, then sends it down a &lt;strong&gt;specialized path&lt;/strong&gt;. Instead of one generalist prompt trying to handle billing questions, technical issues, and small talk equally badly, each category gets a handler tuned for exactly its kind of input.&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%2Ft8hq4oaqvliw1rmy2ad8.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%2Ft8hq4oaqvliw1rmy2ad8.png" alt=" " width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building it in ByteChef
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add an AI model step as the &lt;strong&gt;classifier&lt;/strong&gt;. Set its Response Format to &lt;em&gt;Structured data&lt;/em&gt; and define a Response Schema with a single string field restricted to your categories (&lt;code&gt;billing&lt;/code&gt;, &lt;code&gt;technical&lt;/code&gt;, &lt;code&gt;general&lt;/code&gt;). Constraining the output through the schema is what makes routing dependable - instead of a free-text answer the Branch can't act on, you get one of your category strings back.&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;Branch&lt;/strong&gt; flow control keyed on the classifier's output. Branch executes exactly one path based on the expression value - a visual switch statement. Add a &lt;strong&gt;default branch&lt;/strong&gt; as well: the schema constrains the &lt;em&gt;shape&lt;/em&gt; of the answer, not its judgment, so a classifier can still misfire or return something you didn't plan for.&lt;/li&gt;
&lt;li&gt;In each branch, add the specialized handler: a model step with a category-specific system prompt, a full AI Agent with category-specific tools, or no LLM at all (some routes just need a Slack notification).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn't hypothetical - it's exactly the architecture of our &lt;a href="https://blog.bytechef.io/blogs/ai-email-classifier" rel="noopener noreferrer"&gt;AI Email Classifier&lt;/a&gt; tutorial, which routes incoming emails to Sales, Support, Finance, Operations, and HR using OpenAI structured output plus branching - default branch included. Routing is arguably the most production-proven agentic pattern there is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; inputs fall into distinct categories that benefit from separate handling, and classification is reliable. Bonus: you can route easy categories to a small, cheap model and reserve the expensive one for the hard cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Orchestrator-Workers
&lt;/h2&gt;

&lt;p&gt;The patterns so far have fixed structure - you know at design time which steps run. Orchestrator-workers is for tasks where the required subtasks &lt;strong&gt;can't be predicted in advance&lt;/strong&gt;. A central LLM analyzes the request, breaks it into subtasks dynamically, delegates each one to a specialized worker, and synthesizes the results.&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%2Fq0u5e56n2zjjdywk5zlt.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%2Fq0u5e56n2zjjdywk5zlt.png" alt=" " width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building it in ByteChef
&lt;/h3&gt;

&lt;p&gt;This is where ByteChef's &lt;strong&gt;AI Agent&lt;/strong&gt; component shines, because an AI Agent can use &lt;em&gt;another AI Agent as a tool&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add an AI Agent as the &lt;strong&gt;orchestrator&lt;/strong&gt;. Its system prompt describes its job: analyze the request, decide which specialists to involve, and combine their answers.&lt;/li&gt;
&lt;li&gt;Under its &lt;strong&gt;Tools&lt;/strong&gt; cluster element, add sub-agents - a research agent, a drafting agent, a data-lookup agent - each with its own model, system prompt, and tools. Give each a clear description so the orchestrator knows when to delegate to it.&lt;/li&gt;
&lt;li&gt;The orchestrator now decides &lt;em&gt;at runtime&lt;/em&gt; which workers to call, in what order, and how to merge their outputs. The composition is recursive: sub-agents can have sub-agents of their own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prefer to keep the orchestration explicit on the canvas? There's a workflow-level variant: have an orchestrator model step emit a &lt;strong&gt;structured list of subtasks&lt;/strong&gt;, feed that list into an &lt;strong&gt;Each&lt;/strong&gt; flow control that runs a worker step per subtask in parallel, then aggregate with a final LLM step. You get dynamic decomposition while every execution stays visible in the workflow history, step by step.&lt;/p&gt;

&lt;p&gt;We covered agent composition, the Agent Playbook for testing, and the full cluster-element architecture in &lt;a href="https://blog.bytechef.io/blogs/spring-ai-agent" rel="noopener noreferrer"&gt;the AI Agent deep dive&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; you can't enumerate the subtasks up front - complex research questions, multi-file code changes, requests that span several domains at once.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Evaluator-Optimizer
&lt;/h2&gt;

&lt;p&gt;The last pattern adds something the others lack: &lt;strong&gt;self-correction&lt;/strong&gt;. One LLM generates a response; a second LLM evaluates it against explicit criteria. If the evaluation fails, the feedback goes back to the generator for another attempt - a draft-and-review loop, automated.&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%2Fqjbyikwgtwywoh1ulpwu.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%2Fqjbyikwgtwywoh1ulpwu.png" alt=" " width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building it in ByteChef
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add a &lt;strong&gt;Loop&lt;/strong&gt; flow control and set its &lt;strong&gt;List of Items&lt;/strong&gt; to a fixed three-element list. That caps the attempts at three - if it isn't converging by then, more laps rarely help.&lt;/li&gt;
&lt;li&gt;Inside the loop, add the &lt;strong&gt;generator&lt;/strong&gt; model step. Its prompt includes the task plus, via data pills, the previous attempt and the evaluator's feedback when they exist.&lt;/li&gt;
&lt;li&gt;Add the &lt;strong&gt;evaluator&lt;/strong&gt; model step with structured output: a schema with an &lt;code&gt;evaluation&lt;/code&gt; field constrained to &lt;code&gt;PASS&lt;/code&gt; / &lt;code&gt;NEEDS_IMPROVEMENT&lt;/code&gt; and a &lt;code&gt;feedback&lt;/code&gt; string. Give it concrete criteria - "evaluate for correctness, completeness, and tone" beats "is this good?".&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;Condition&lt;/strong&gt; on the evaluation. On &lt;code&gt;PASS&lt;/code&gt;, put a &lt;strong&gt;Loop Break&lt;/strong&gt; in that branch - it ends the enclosing loop immediately and you carry on with the accepted result. Otherwise the loop runs again, and the generator sees the fresh feedback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two different prompts - or even two different models - playing generator and critic consistently outperforms a single model trying to self-assess in one call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; you have clear evaluation criteria and the output is worth iterating on - customer-facing copy, generated code, translations, anything with a quality bar. Skip it for cheap, low-stakes outputs; the extra LLM calls should buy you measurable quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Spring AI Under the Hood Matters
&lt;/h2&gt;

&lt;p&gt;The Spring AI post closes by highlighting what the framework contributes to these patterns. Because ByteChef is built on Spring AI, you inherit every one of those advantages - plus a few that only a visual platform can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model portability.&lt;/strong&gt; Spring AI's &lt;code&gt;ChatModel&lt;/code&gt; abstraction normalizes providers, so in ByteChef swapping OpenAI for Claude, Gemini, or a local Ollama model is a dropdown change, not a refactor. Build the pattern once, A/B the model later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output.&lt;/strong&gt; Every response schema you define in a ByteChef AI step rides on Spring AI's structured-output machinery. Routing and evaluator-optimizer depend on this - patterns fall apart when the classifier answers with an essay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A consistent, maintained foundation.&lt;/strong&gt; New providers, new vector stores, and new capabilities land in Spring AI continuously, and ByteChef picks them up - without you rewriting workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And what ByteChef adds on top:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observability for free.&lt;/strong&gt; Every pattern above produces a step-by-step execution history: every prompt, every intermediate output, every branch decision, inspectable per run. No logging code required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools without glue code.&lt;/strong&gt; Any action from ByteChef's 280+ connectors can be handed to an agent as a tool - the orchestrator pattern gets Slack, Salesforce, and Google Sheets access in a few clicks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails and testing built in.&lt;/strong&gt; Wrap any agent in content and topic guardrails, and test it live in the &lt;a href="https://blog.bytechef.io/blogs/spring-ai-agent" rel="noopener noreferrer"&gt;Agent Playbook&lt;/a&gt; while you build.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Choosing the Right Pattern
&lt;/h2&gt;

&lt;p&gt;Chain - Fixed, sequential - The task has clear stages that build on each other&lt;br&gt;
Parallelization - Fixed, concurrent  -Subtasks are independent, or you want multiple perspectives&lt;br&gt;
Routing - Fixed paths, dynamic choice - Inputs fall into categories needing different handling&lt;br&gt;
Orchestrator-workers - Dynamic - Subtasks can't be predicted at design time&lt;br&gt;
Evaluator-optimizer - Iterative - Clear quality criteria exist, and iteration measurably helps |&lt;/p&gt;

&lt;p&gt;Anthropic's guidance - echoed by the Spring AI team, and just as true on a canvas as in Java - is worth repeating: &lt;strong&gt;start with the simplest pattern that could work&lt;/strong&gt;. A well-prompted single LLM step beats a five-agent system that nobody can debug. Add parallelization when latency hurts, routing when one prompt stops fitting all inputs, and orchestration only when the task genuinely demands dynamic decomposition. Complexity should be earned.&lt;/p&gt;

&lt;p&gt;The nice thing about building these patterns visually is that &lt;em&gt;upgrading&lt;/em&gt; between them is cheap: a chain becomes a routing workflow by dropping in one classifier step and a Branch; a single agent becomes an orchestrator by adding sub-agents to its Tools. Your architecture can grow exactly as fast as your use case does.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>bytechef</category>
      <category>spring</category>
    </item>
    <item>
      <title>One Workflow, Many Lanes: Completing ByteChef's Flow Controls</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Fri, 14 Aug 2026 18:36:57 +0000</pubDate>
      <link>https://dev.to/bytechef/one-workflow-many-lanes-completing-bytechefs-flow-controls-4k0f</link>
      <guid>https://dev.to/bytechef/one-workflow-many-lanes-completing-bytechefs-flow-controls-4k0f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; ByteChef's workflow editor now exposes the &lt;strong&gt;full set of flow controls&lt;/strong&gt;: alongside the familiar &lt;strong&gt;Condition&lt;/strong&gt;, &lt;strong&gt;Branch&lt;/strong&gt;, and &lt;strong&gt;Loop&lt;/strong&gt;, you can drop &lt;strong&gt;Parallel&lt;/strong&gt;, &lt;strong&gt;Fork/Join&lt;/strong&gt;, &lt;strong&gt;Each&lt;/strong&gt;, &lt;strong&gt;Map&lt;/strong&gt;, and &lt;strong&gt;Subflow&lt;/strong&gt; onto the canvas. That means workflows that fan out over lists, run independent steps concurrently, and call other workflows as reusable building blocks - all visually, no custom code. This closes out &lt;a href="https://github.com/bytechefhq/bytechef/issues/1057" rel="noopener noreferrer"&gt;issue #1057&lt;/a&gt;, one of the longest-running feature checklists in the ByteChef repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Some GitHub issues are essays. &lt;a href="https://github.com/bytechefhq/bytechef/issues/1057" rel="noopener noreferrer"&gt;Issue #1057&lt;/a&gt; is a checklist:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;[x] condition&lt;/li&gt;
&lt;li&gt;[x] loop&lt;/li&gt;
&lt;li&gt;[x] each&lt;/li&gt;
&lt;li&gt;[x] branch&lt;/li&gt;
&lt;li&gt;[x] map&lt;/li&gt;
&lt;li&gt;[x] parallel&lt;/li&gt;
&lt;li&gt;[x] fork-join&lt;/li&gt;
&lt;li&gt;[x] subflow&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each of those checkboxes is a &lt;strong&gt;flow control&lt;/strong&gt; - what the workflow engine internally calls a &lt;em&gt;task dispatcher&lt;/em&gt;. A regular component does work: it sends the email, queries the database, calls the API. A task dispatcher never does work itself. It decides &lt;strong&gt;which tasks run, when, how many times, and with what data&lt;/strong&gt; - it directs traffic.&lt;/p&gt;

&lt;p&gt;We wrote about the first half of that checklist in &lt;a href="https://blog.bytechef.io/blogs/guide-to-flow-controls" rel="noopener noreferrer"&gt;our guide to flow controls&lt;/a&gt;: &lt;strong&gt;Condition&lt;/strong&gt; routes on true/false, &lt;strong&gt;Branch&lt;/strong&gt; picks one of several paths based on an expression, and &lt;strong&gt;Loop&lt;/strong&gt; repeats steps over a list. Those cover &lt;em&gt;decisions&lt;/em&gt; and &lt;em&gt;repetition&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This post is about the second half - the controls that cover &lt;strong&gt;concurrency&lt;/strong&gt; and &lt;strong&gt;composition&lt;/strong&gt;. They've been running behind a feature flag while we hardened them one checkbox at a time; with the list complete, the flag is going away and the full set is available to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Sequential Isn't Always Enough
&lt;/h2&gt;

&lt;p&gt;Every workflow starts as a straight line: trigger, then step one, then step two. That's the right default - it's easy to reason about, and each step can use the output of the one before it.&lt;/p&gt;

&lt;p&gt;But real processes aren't always lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Onboarding a customer means creating a CRM record, provisioning an account, &lt;em&gt;and&lt;/em&gt; notifying the sales channel - three things that don't depend on each other, so why wait?&lt;/li&gt;
&lt;li&gt;Enriching 200 leads one at a time takes 200× as long as enriching them all at once.&lt;/li&gt;
&lt;li&gt;Five different workflows all end with the same "notify the team" sequence, and you're tired of rebuilding it five times.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first two are concurrency problems. The third is a composition problem. Here's the control for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel: Independent Steps, All at Once
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Parallel&lt;/strong&gt; is the simplest of the new controls: give it a collection of tasks, and it runs them all concurrently without waiting for any of them to finish first.&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;"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;"parallel_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;"parallel/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;"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;"createCrmRecord"&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;"pipedrive/v1/createOrganization"&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;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"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;"notifySales"&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/v2/sendMessage"&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;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;Use it when you have a &lt;em&gt;fixed set of different steps&lt;/em&gt; that don't depend on each other. The three-things-at-customer-onboarding case is exactly this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fork/Join: Parallel Branches, Sequential Inside
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fork/Join&lt;/strong&gt; is Parallel's bigger sibling. Instead of a flat set of tasks, you define &lt;em&gt;branches&lt;/em&gt; - each branch is a sequence of tasks that runs in order, but the branches themselves run in parallel to each other, each as its own isolated sub-flow:&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;"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;"forkJoin_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;"fork-join/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;"branches"&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;"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;"fetchInvoices"&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;"..."&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;"summarizeInvoices"&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;"..."&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;"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;"fetchTickets"&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;"..."&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;"summarizeTickets"&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;"..."&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 rule of thumb: reach for &lt;strong&gt;Parallel&lt;/strong&gt; when each concurrent piece is a single step, and for &lt;strong&gt;Fork/Join&lt;/strong&gt; when each concurrent piece is itself a &lt;em&gt;pipeline&lt;/em&gt;. The "join" part means the workflow waits for every branch to finish before moving on, so the step after a Fork/Join can safely use results from all branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Each and Map: Fan Out Over a List
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Loop&lt;/strong&gt; already iterates over a list - but sequentially, one item at a time, in order. &lt;strong&gt;Each&lt;/strong&gt; and &lt;strong&gt;Map&lt;/strong&gt; iterate over a list &lt;em&gt;in parallel&lt;/em&gt;: every item gets its own execution of the inner task at the same time.&lt;/p&gt;

&lt;p&gt;The difference between the two is what you get back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each&lt;/strong&gt; is for side effects. It runs the task for every item and returns nothing. Order of completion isn't guaranteed - and for firing off 200 notification emails, it doesn't need to be:&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;"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;"each_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;"each/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;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"=leads"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"iteratee"&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;"sendFollowUp"&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;"gmail/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="s2"&gt;"=each_1.item.email"&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;&lt;strong&gt;Map&lt;/strong&gt; is for transformations. It also runs in parallel, but it &lt;em&gt;collects&lt;/em&gt; each item's result and returns them as a list - in an order that matches the source list, no matter which items finished first:&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;"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;"map_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;"map/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;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"=range(1, 10)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"iteratee"&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;"enrich"&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;"..."&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;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"=map_1.item"&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;Inside the iteration, the current element is available as a data pill on the dispatcher itself - &lt;code&gt;each_1.item&lt;/code&gt;, &lt;code&gt;map_1.item&lt;/code&gt; - so inner steps can reference it like any other output.&lt;/p&gt;

&lt;p&gt;So the lead-enrichment case from earlier: 200 leads, enriched concurrently, results back in the original order, ready for the next step. That's Map.&lt;/p&gt;

&lt;p&gt;One honest footnote on the checklist: the &lt;em&gt;infinite loop&lt;/em&gt; variant (loop until a break condition, with no list at all) is the one box still open on #1057. Loop already supports a &lt;strong&gt;Loop Break&lt;/strong&gt; statement; the fully unbounded mode is still on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subflow: Workflows Calling Workflows
&lt;/h2&gt;

&lt;p&gt;The controls above change &lt;em&gt;how&lt;/em&gt; tasks run. &lt;strong&gt;Subflow&lt;/strong&gt; changes &lt;em&gt;what counts as a task&lt;/em&gt;: it starts another workflow as a child job of the current one, passes it inputs, and hands the child's output back as the step's output.&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;"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;"subflow_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;"subflow/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;"workflowUuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notify-the-team"&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="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;"#ops"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"=map_1"&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 composition problem solved: build "notify the team" once, with its own inputs, and call it from all five workflows. When it changes, it changes everywhere. Subflows show up in execution history as their own jobs, and the editor knows how to &lt;a href="https://blog.bytechef.io/blogs/building-subflow-navigation" rel="noopener noreferrer"&gt;navigate into them&lt;/a&gt; so you can follow the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which One Do I Reach For?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want to…&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repeat steps over a list, one at a time, in order&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Loop&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run a task for every item at once, no results needed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Each&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transform every item at once and keep the results, in order&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Map&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run a fixed set of independent single steps concurrently&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Parallel&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run several multi-step pipelines concurrently, then continue&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fork/Join&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reuse another workflow as a step&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Subflow&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Take one of two paths based on true/false&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Condition&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Take one of many paths based on a value&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Branch&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two quick heuristics cover most decisions: &lt;em&gt;per-item vs. fixed set&lt;/em&gt; (Each/Map/Loop iterate over data; Parallel/Fork-Join run a structure you defined), and &lt;em&gt;results vs. side effects&lt;/em&gt; (Map collects, Each doesn't).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Feature Flag, and Why It's Leaving
&lt;/h2&gt;

&lt;p&gt;The workflow engine has been able to dispatch all of these for a while - the engine side of a task dispatcher is comparatively contained. The long tail was the &lt;strong&gt;editor&lt;/strong&gt;. Every flow control is a nested structure on the canvas: it owns child tasks, those children need placeholders, drag-and-drop targets, correct auto-layout, and data pills that respect iteration scope. Getting Condition right taught us how much surface area each control adds, so we didn't ship the rest as one big drop.&lt;/p&gt;

&lt;p&gt;Instead, the remaining controls went in behind a feature flag (&lt;code&gt;ff-1057&lt;/code&gt;, named after the issue), which let us enable them incrementally, polish the rough edges. With every control now holding up in real workflows, the flag is being removed and the complete set becomes the default for every ByteChef instance - cloud and self-hosted.&lt;/p&gt;

&lt;p&gt;If your workflows have been running in a single lane, &lt;a href="https://app.bytechef.io/" rel="noopener noreferrer"&gt;give the new controls a try&lt;/a&gt; - the fastest way to feel the difference is to take an existing Loop over an independent list of items and swap it for a Map.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>bytechef</category>
      <category>agents</category>
    </item>
    <item>
      <title>Stop the Workflow, Ask a Human: Human-in-the-Loop Approvals in ByteChef</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Thu, 13 Aug 2026 15:20:34 +0000</pubDate>
      <link>https://dev.to/bytechef/stop-the-workflow-ask-a-human-human-in-the-loop-approvals-in-bytechef-1hbf</link>
      <guid>https://dev.to/bytechef/stop-the-workflow-ask-a-human-human-in-the-loop-approvals-in-bytechef-1hbf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; ByteChef's &lt;strong&gt;Approval component&lt;/strong&gt; pauses a running workflow until a person approves or rejects, optionally filling in a form whose answers flow straight into the next steps. Delivery is pluggable: Slack (with inline Approve/Discard buttons), Gmail, Outlook, Discord, Telegram, WhatsApp, and more - or ByteChef's new &lt;strong&gt;in-app Approval Tasks inbox&lt;/strong&gt;. The approver clicks a signed link and needs no ByteChef account, and the workflow waits up to 60 days without holding a thread - &lt;strong&gt;human-in-the-loop (HITL)&lt;/strong&gt;, with no infrastructure of your own.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You build a workflow, and it runs beautifully - right up to the step you don't actually want it to run on its own. Issue the refund? Create the customer account? Book the leave?&lt;/p&gt;

&lt;p&gt;Some steps shouldn't happen without a human's yes. What you want is simple to say: the workflow pauses, the right person gets pinged where they already work, they approve - maybe fill in a couple of fields - and the workflow picks up exactly where it stopped, with their answer in hand.&lt;/p&gt;

&lt;p&gt;That pattern has a name - &lt;strong&gt;human-in-the-loop (HITL)&lt;/strong&gt; - and in ByteChef it's the &lt;strong&gt;Approval component&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you've read our use-case tutorials, you've already met it without an introduction. The &lt;a href="https://blog.bytechef.io/blogs/automating-kyc-compliance-building-a-smart-onboarding-workflow-in-bytechef" rel="noopener noreferrer"&gt;KYC compliance workflow&lt;/a&gt; gates onboarding on a compliance officer's review. The &lt;a href="https://blog.bytechef.io/blogs/building-liferay-integrations-in-bytechef" rel="noopener noreferrer"&gt;Liferay onboarding workflow&lt;/a&gt; waits for a manager to fill in account details. &lt;a href="https://blog.bytechef.io/blogs/pto-elf" rel="noopener noreferrer"&gt;PTO Elf&lt;/a&gt; holds leave requests for sign-off.&lt;/p&gt;

&lt;p&gt;In each of those, approval was a supporting actor: "add an approval task here" and the tutorial moved on. This post is the missing introduction - what you configure, what your approver sees, and what the platform quietly does for you in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding an Approval Step
&lt;/h2&gt;

&lt;p&gt;In the workflow editor, Approval is one component with one action: &lt;strong&gt;Request Approval&lt;/strong&gt;. The minimal setup is no setup at all - leave everything blank and your approver simply gets Approve and Reject buttons.&lt;/p&gt;

&lt;p&gt;Usually, you'll give it a form title, a description, and a list of form inputs. The form builder supports thirteen field types: text, textarea, number, email, password, checkbox, radio, select, date and datetime pickers, file upload, hidden fields, and raw custom HTML for anything else.&lt;/p&gt;

&lt;p&gt;Each field carries the knobs you'd expect from a form builder: a label and description, a placeholder, a default value, and a required flag. Radio and select fields take label/value options, and a select can allow multiple choices with a minimum and maximum selection count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Approver's Answers Become Data Pills
&lt;/h2&gt;

&lt;p&gt;Here's the part that makes forms genuinely useful downstream: the approval step's output schema is generated from the fields you configure.&lt;/p&gt;

&lt;p&gt;Add an &lt;code&gt;accountName&lt;/code&gt; text field and a &lt;code&gt;creditLimit&lt;/code&gt; number field, and the step's output is &lt;code&gt;{approved: boolean, accountName: string, creditLimit: number}&lt;/code&gt;. Every one of those shows up as a data pill for the steps that follow.&lt;/p&gt;

&lt;p&gt;That's how the Liferay tutorial creates an account "from the information received from the onboarding approval task" - the approver's answers are just the step's output, referenced like any other: &lt;code&gt;${approval_1.accountName}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Approver Finds Out
&lt;/h2&gt;

&lt;p&gt;A paused workflow is useless if nobody knows it's waiting. That's what &lt;strong&gt;approval channels&lt;/strong&gt; are for - delivery mechanisms you attach to the approval step, right under the node's Channels port.&lt;/p&gt;

&lt;p&gt;Most channels deliver the approval link to people &lt;em&gt;outside&lt;/em&gt; ByteChef - a manager who has never opened the app and never will. Pick where your approvers already live: &lt;strong&gt;Slack&lt;/strong&gt;, &lt;strong&gt;Gmail&lt;/strong&gt;, &lt;strong&gt;Outlook 365&lt;/strong&gt;, plain &lt;strong&gt;Email&lt;/strong&gt; (SMTP), &lt;strong&gt;Discord&lt;/strong&gt;, &lt;strong&gt;Telegram&lt;/strong&gt;, &lt;strong&gt;WhatsApp&lt;/strong&gt;, &lt;strong&gt;Twilio&lt;/strong&gt; (SMS), &lt;strong&gt;Mattermost&lt;/strong&gt;, &lt;strong&gt;Rocket.Chat&lt;/strong&gt;, &lt;strong&gt;Infobip&lt;/strong&gt;, or the embeddable &lt;strong&gt;Chat&lt;/strong&gt; widget. Each uses its own connection.&lt;/p&gt;

&lt;p&gt;The Slack channel has my favorite detail. If your approval has no form fields, there's no reason to send anyone to a web page, so it doesn't. Approvers get &lt;strong&gt;Approve&lt;/strong&gt; and &lt;strong&gt;Discard&lt;/strong&gt; buttons right in the Slack message, and the approval resolves without leaving the chat. If there &lt;em&gt;is&lt;/em&gt; a form, the message carries a single "Open Approval Form" button instead.&lt;/p&gt;

&lt;p&gt;One channel is different: &lt;strong&gt;Approval Task&lt;/strong&gt;. Instead of sending the link out, it creates a task &lt;em&gt;inside&lt;/em&gt; ByteChef, on the new &lt;strong&gt;Approval Tasks&lt;/strong&gt; page - an approval inbox for teams that live in the app.&lt;/p&gt;

&lt;p&gt;Each task has a status (open, in progress, completed), a priority, an assignee, and a due date, and the page supports filtering, search, and sorting. If your approvers are ByteChef users, their pending approvals queue up in one place instead of scattering across inboxes.&lt;/p&gt;

&lt;p&gt;One small courtesy across all channels: they're skipped when the step runs inside the workflow editor's test mode, so testing a workflow doesn't spam your approvers.&lt;/p&gt;

&lt;p&gt;And because channels are plug-ins (cluster elements) rather than baked-in integrations, the delivery list can grow without touching the Approval component itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Approver Experiences
&lt;/h2&gt;

&lt;p&gt;Your approver clicks a link and lands on a form page: title, description, fields, and environment badge. No login, no account, no "first, install ByteChef" - which is the point, because the person approving is often exactly the person who'd never log in.&lt;/p&gt;

&lt;p&gt;That works because the link itself is the credential. The token in the URL wraps the approval's identity in an HMAC signature, so it can't be guessed or forged - the same trust model as any webhook callback. Possessing the link &lt;em&gt;is&lt;/em&gt; the authorization.&lt;/p&gt;

&lt;p&gt;A link is good for exactly one decision. Once the approval is submitted - or the 60-day window passes - the link answers &lt;code&gt;410 Gone&lt;/code&gt;, so a stale email can't resurrect a settled question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meanwhile, the Workflow Waits
&lt;/h2&gt;

&lt;p&gt;While your approver takes their time, what is the workflow doing? Nothing - and that's the engineering trick worth knowing about.&lt;/p&gt;

&lt;p&gt;The approval action doesn't block a thread, poll a flag, or sleep. It registers a suspension with the engine and returns; the engine parks the execution, persists everything, and releases the worker. The workflow can stay parked for up to 60 days; the server can restart, redeploy, or scale to zero in the meantime. Nothing waits in memory.&lt;/p&gt;

&lt;p&gt;When the approver submits, the engine re-enters the action through a second entry point with the submitted data, and the workflow resumes exactly where it stopped - the form answers flattened into the output schema those data pills promised.&lt;/p&gt;

&lt;p&gt;Long-time users might remember the old ways of doing this: a dedicated &lt;code&gt;wait-for-approval&lt;/code&gt; flow control and a &lt;code&gt;Create Approval Links&lt;/code&gt; action that handed out bare approve/reject URLs. Both are now deprecated, because a flow control can pause but it can't &lt;em&gt;be a component&lt;/em&gt; - with a form builder, a typed output, delivery channels, and one more trick below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming Next: The Same Step, Inside an AI Agent
&lt;/h2&gt;

&lt;p&gt;Human-in-the-loop matters most where the human is easiest to lose: AI agents. An autonomous agent is exactly as trustworthy as its worst tool call, and "pause and ask a person" is the simplest guardrail there is.&lt;/p&gt;

&lt;p&gt;The suspend-and-resume machinery above is exactly what an agent needs, so we're working on exposing Approval as a &lt;strong&gt;tool&lt;/strong&gt; you can hand to a ByteChef AI agent: the agent decides it needs sign-off, composes the form, calls the tool, and the whole agent conversation parks until a human answers. From the model's perspective, it asked a tool a question and the tool answered - it just took a day and a half.&lt;/p&gt;

&lt;p&gt;It's not in today's build - we'd rather ship it solid than early - but it's close, and it lands in one of the next releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You'd Reach for It
&lt;/h2&gt;

&lt;p&gt;The use-case posts each show one pattern, and they generalize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gate&lt;/strong&gt;: Nothing proceeds without a yes/no compliance review before account creation (KYC) or manager sign-off before booking PTO. No form fields needed; Slack buttons suffice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate with input&lt;/strong&gt;: the approver doesn't just consent; they &lt;em&gt;contribute&lt;/em&gt; - the Liferay onboarding manager supplies the account name and description the next step uses to create the account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold escalation&lt;/strong&gt;: the workflow handles the routine cases on its own and branches into an approval only for the consequential ones - refunds over a limit, contract changes, anything you'd want a human to see before it happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see approval in a full workflow, the &lt;a href="https://blog.bytechef.io/blogs/automating-kyc-compliance-building-a-smart-onboarding-workflow-in-bytechef" rel="noopener noreferrer"&gt;KYC compliance&lt;/a&gt; and &lt;a href="https://blog.bytechef.io/blogs/building-liferay-integrations-in-bytechef" rel="noopener noreferrer"&gt;Liferay onboarding&lt;/a&gt; posts both build one end-to-end, and now you know what's happening under the pause.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>bytechef</category>
      <category>agents</category>
    </item>
    <item>
      <title>See What Your Workflow Was Thinking: Surfacing Component Logs in ByteChef</title>
      <dc:creator>ByteChef</dc:creator>
      <pubDate>Fri, 07 Aug 2026 19:55:03 +0000</pubDate>
      <link>https://dev.to/bytechef/see-what-your-workflow-was-thinking-surfacing-component-logs-in-bytechef-362</link>
      <guid>https://dev.to/bytechef/see-what-your-workflow-was-thinking-surfacing-component-logs-in-bytechef-362</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; ByteChef's &lt;strong&gt;Logger component&lt;/strong&gt; could always write log messages - but they vanished into the server console, where workflow builders could never see them. We fixed that by capturing log calls at the layer &lt;em&gt;every&lt;/em&gt; component passes through, storing them per execution, and exposing them in a new &lt;strong&gt;Logs tab&lt;/strong&gt; in both the workflow builder and the execution history. Every component's logs - not just Logger's - now show up next to the step's Input and Output, with level badges, JSON pretty-printing, and expandable stack traces.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every automation platform eventually gets this ticket. Ours was &lt;a href="https://github.com/bytechefhq/bytechef/issues/2896" rel="noopener noreferrer"&gt;issue #2896&lt;/a&gt;, and it was refreshingly short:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;As a Workflow Developer, I want to see logs from the Logger component in the workflow builder and workflow executions, so that I can see details of how my workflows are executing.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ByteChef has had a &lt;a href="https://www.bytechef.io/" rel="noopener noreferrer"&gt;Logger component&lt;/a&gt; for a long time. You drop it between two steps, give it a message - usually with a few data pills in it - and pick a level: &lt;strong&gt;Debug&lt;/strong&gt;, &lt;strong&gt;Info&lt;/strong&gt;, &lt;strong&gt;Warn&lt;/strong&gt;, or &lt;strong&gt;Error&lt;/strong&gt;. That's the whole component.&lt;/p&gt;

&lt;p&gt;The problem: the message went to the server log and nowhere else. If you self-hosted ByteChef and had shell access, you could tail the server log and find it. If you were a workflow builder working in the UI - the person the Logger component exists for - it went into a void. The component technically worked and was practically useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding Where to Capture
&lt;/h2&gt;

&lt;p&gt;The obvious fix would have been to special-case the Logger component: make it return its message as step output, or write to some Logger-specific store. We went a different way, and it's the decision the whole feature hangs on.&lt;/p&gt;

&lt;p&gt;Every component action in ByteChef receives an execution context, and that context's logging method is the sanctioned way for &lt;em&gt;any&lt;/em&gt; component to log - the HTTP client logs request failures, Script components log from user code, AI components log token usage warnings. All of those messages were equally invisible.&lt;/p&gt;

&lt;p&gt;So instead of touching the Logger component at all, we instrumented the logging implementation inside the component context - the single funnel that every log call in every component already passes through. Each message still goes to the server log like before, but now it also becomes a structured entry: timestamp, level, component name, operation, the step it came from, the message, and - when there is one - the exception type, message, and stack trace.&lt;/p&gt;

&lt;p&gt;Because the capture point sits below every component, the feature ships for all 180+ components at once. The Logger component didn't change by a single line. It just became visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing Logs: One File per Execution
&lt;/h2&gt;

&lt;p&gt;Logs are append-heavy, read-rarely data with an obvious partition key: the job - a single workflow execution. That ruled out a database table. We didn't want every workflow run inserting rows into PostgreSQL for data that's mostly never queried.&lt;/p&gt;

&lt;p&gt;Instead, each job gets one file in ByteChef's existing file-storage abstraction, which means logs land wherever your deployment already puts files - the filesystem, a database blob, or S3-compatible storage. The format is JSON Lines: one JSON object per line, so appending an entry is just serialize-and-append.&lt;/p&gt;

&lt;p&gt;Two properties of the writer matter more than they look.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;writes are asynchronous&lt;/strong&gt;. Logging must never slow down the workflow itself - a component that logs five times shouldn't pay five rounds of storage I/O on the execution path. Java 25's virtual threads make that fire-and-forget pattern nearly free.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;the read path is guarded by the same permission model as everything else that touches executions&lt;/strong&gt;. Logs frequently contain payload data - that's the point of logging them - so they inherit the exact visibility rules of the execution they belong to. If you can't see the run, you can't see its logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Environments, Two Pipelines
&lt;/h2&gt;

&lt;p&gt;There's a wrinkle: ByteChef has two kinds of "execution". There are &lt;strong&gt;production runs&lt;/strong&gt; of deployed workflows, and there are &lt;strong&gt;test runs&lt;/strong&gt; you trigger with the Test button while building a workflow in the editor. Test runs are ephemeral - their logs shouldn't accumulate in long-term storage next to production history.&lt;/p&gt;

&lt;p&gt;So the feature is split down the middle. Production runs write to the long-term log area and are served to the execution history view. Editor runs write to a separate area with their own cleanup, served by a parallel query. The context factory decides which writer a component gets based on which environment it's running in, and the client picks the matching query. Same UI, same log format, different lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Read API
&lt;/h2&gt;

&lt;p&gt;The read API is GraphQL, matching where the rest of ByteChef's newer client-server surface has been heading. It supports pagination and filtering from day one: minimum level, component name, a specific step, a time range, and free-text search.&lt;/p&gt;

&lt;p&gt;The per-step filter is what powers the most-used view: click a node in the execution panel, and you see only that step's logs. Look at the job as a whole, and you see everything, with a small component-name chip on each row so you can tell who said what.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Logs Tab
&lt;/h2&gt;

&lt;p&gt;On the client, the execution detail panel - the one with &lt;strong&gt;Input&lt;/strong&gt; and &lt;strong&gt;Output&lt;/strong&gt; tabs you get when you click a step - gains a third tab: &lt;strong&gt;Logs&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%2Fblog.bytechef.io%2F_next%2Fimage%3Furl%3D%252Fblogs%252Fsurfacing-workflow-execution-logs%252Flogs-tab.png%26w%3D3840%26q%3D75" 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%2Fblog.bytechef.io%2F_next%2Fimage%3Furl%3D%252Fblogs%252Fsurfacing-workflow-execution-logs%252Flogs-tab.png%26w%3D3840%26q%3D75" alt="Alt Text" width="3010" height="2020"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's a real run above - and it's worth noting whose log you're looking at. The workflow has a Logger component in it, sitting right there in the graph on the right. But the entry on screen came from the &lt;strong&gt;OpenAI&lt;/strong&gt; step, which logged its outgoing request without anyone asking it to. That's the capture-one-layer-down decision paying off in a screenshot: the component that made the ticket necessary isn't even the one being shown.&lt;/p&gt;

&lt;p&gt;Each row shows a timestamp, a colored level badge (blue &lt;code&gt;INFO&lt;/code&gt;, purple &lt;code&gt;DEBUG&lt;/code&gt;, yellow &lt;code&gt;WARN&lt;/code&gt;, red &lt;code&gt;ERROR&lt;/code&gt;), and the message. Two small touches make it feel finished rather than bolted on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON detection.&lt;/strong&gt; People constantly log entire objects - dumping a payload into a log line is a very common debugging move. If a message parses as JSON, the tab renders it with the same collapsible tree viewer we use for step outputs, instead of an unreadable one-liner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expandable errors.&lt;/strong&gt; When an entry carries exception details, the row becomes clickable and expands into the exception type, message, and full stack trace - so a failed step's story is readable in place, without asking an operator to grep the server log for you.&lt;/p&gt;

&lt;p&gt;And because the editor pipeline exists, the same tab appears in the workflow builder's test output panel. Run a workflow with the Test button, click a step, open Logs - the feedback loop that issue #2896 asked for, closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Layer Down
&lt;/h2&gt;

&lt;p&gt;The lesson we're taking from this one: when a ticket says "make the Logger component's output visible", the right fix might be one layer down. By instrumenting the context instead of the component, a request about one component became execution-level observability for every component in the platform - including the ones we haven't built yet.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>bytechef</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
