guides

ByteChef Embedded, Part 9: New from Chat

ByteChef Embedded, Part 9: New from Chat
22 min read
Ivica Čardić

TL;DR: New from Chat in the embedded sample app is a chat page where your user builds a workflow by talking to ByteChef's workflow agent. It's one React component, EmbeddedWorkflowChat, that takes the connected user's JWT, a list of starter suggestions, an optional system prompt with your house rules, and an onWorkflowReady callback. On mount it creates an empty draft workflow; every turn after that streams over AG-UI from POST /automation/workflows/{uuid}/copilot/chat, and the agent edits that draft directly. When it needs something from the user it renders an interactive card in the thread: a multiple-choice question, an option picker, a connection picker, or a Connect button that opens the ConnectDialog from part one. When the first turn finishes, onWorkflowReady(uuid) fires and you can send the user to the editor. This is part nine of the series.

In part eight your users started from a workflow you built for them. That's the fastest path, but it only works when you guessed their automation in advance. Often you didn't. Real automations are discovered as much as they're declared: "when a deal closes... actually only enterprise deals... and loop in the account manager... and skip weekends."

That back-and-forth is what New from Chat is for. Your user describes what they want in plain language, and ByteChef's workflow agent builds it in their account while they watch. It adds a trigger, wires steps, looks up the actions it needs, asks when something is ambiguous, and stops to let the user connect an app when a credential is missing. The user never sees a canvas unless they want to.

Where Chat Fits

Open Automations in the sample app. The split button in the top right lists every way to start a workflow:

Sample app Workflows page with the create menu open, listing New from Template, New from Embedded Workflow Builder, New from Prompt, New from Chat and New from Custom Workflow Builder

Here's how the options compare:

OptionThe user...Best when
New from Template (part eight)Picks a workflow you builtYou know the common automations up front
New from Embedded Workflow BuilderDrags nodes on ByteChef's canvasThe user is comfortable with visual builders
New from PromptWrites one description, gets one resultThe request is short and unambiguous
New from Chat (this part)Talks it through with an agent, turn by turnThe request is fuzzy, or the user doesn't know the apps' vocabulary
New from Custom Workflow Builder (part seven)Fills in a UI you designedYou want full control of the experience

Chat is the most forgiving option for non-technical users. They don't need to know that Slack's action is called Send Channel Message or that a schedule trigger has a dayOfWeek property. The agent knows, and when it isn't sure what the user means, it asks.

The Chat at a Glance

Click New from Chat and you land on /automations/chat:

Sample app New from Chat page showing a collapsed System prompt (optional) disclosure, the heading How can I help you today?, a message input and four suggestion chips

The page has three parts:

  • A collapsed System prompt (optional) field at the top, where the integrator (you) adds house rules. More on that in Your House Rules.
  • The chat itself, centered while it's empty, with the heading How can I help you today? and a message box.
  • Suggestion chips under the message box. Clicking one sends its text as the user's first message, so a blank chat isn't intimidating.

Once the conversation starts, the message box docks to the bottom of the page and the thread fills the space above it. As soon as the first turn finishes, an Open workflow button appears in the top right corner.

Mounting the Component

Here's the whole page from the sample, app/automations/chat/page.tsx, trimmed to the parts that matter:

export default function GenerateFromChatPage() {
    const [jwtToken, setJwtToken] = useState<string | null>(null);
    const [systemPrompt, setSystemPrompt] = useState('');
    const [workflowUuid, setWorkflowUuid] = useState<string | null>(null);

    const router = useRouter();

    useEffect(() => {
        getToken().then(setJwtToken);
    }, []);

    return (
        <div className="flex h-screen w-full flex-col p-4">
            <div className="mb-3 flex items-center justify-between gap-4">
                <details className="flex-1 text-sm">
                    <summary>System prompt (optional)</summary>

                    <Textarea
                        onChange={(event) => setSystemPrompt(event.target.value)}
                        placeholder="e.g. Always prefer Slack over email; keep workflows under five steps."
                        value={systemPrompt}
                    />
                </details>

                {workflowUuid && (
                    <Button onClick={() => router.push(`/automations/${workflowUuid}`)}>Open workflow</Button>
                )}
            </div>

            <div className="min-h-0 flex-1">
                {jwtToken && (
                    <EmbeddedWorkflowChat
                        baseUrl={BYTECHEF_APP_BASE_URL}
                        environment={BYTECHEF_ENVIRONMENT}
                        jwtToken={jwtToken}
                        onWorkflowReady={setWorkflowUuid}
                        suggestions={[
                            {
                                title: 'Search for an action',
                                label: 'that can send an email',
                                action: 'Search for an action that can send an email',
                            },
                            {
                                title: 'How do I implement',
                                label: 'conditional branching in workflows?',
                                action: 'How do I implement conditional branching in workflows?',
                            },
                            // ...
                        ]}
                        systemPrompt={systemPrompt}
                    />
                )}
            </div>
        </div>
    );
}

getToken() is the same helper the whole sample uses. It asks the sample's back-end to sign a connected-user JWT, exactly as in part one. The chat waits for the token before it mounts, because everything it does is on behalf of that user.

Props

PropTypeWhat it does
jwtTokenstring (required)The connected user's JWT. Sent as Authorization: Bearer ... on every request
baseUrlstringYour ByteChef instance. Defaults to https://app.bytechef.io
environment'DEVELOPMENT' | 'STAGING' | 'PRODUCTION'Sent as X-Environment. Defaults to PRODUCTION, so pass it explicitly while you develop
suggestions{title, label, action}[]Starter chips. title and label are shown side by side; action is the message that gets sent
systemPromptstringYour house rules, applied to every turn
onWorkflowReady(workflowUuid: string) => voidCalled once, after the agent's first turn finishes
classNamestringExtra classes for the outer container

The component fills its parent's height, so give it a parent with a fixed or flexible height (the sample uses min-h-0 flex-1 inside a h-screen column).

Where the Component Comes From

Unlike useConnectDialog or EmbeddedWorkflowBuilder, the chat isn't an npm export. It lives as source in the sample app, under front-end/src/components/embedded-workflow-chat/, and you copy the folder into your project:

embedded-workflow-chat/
├── index.ts                            # exports the component
├── EmbeddedWorkflowChat.tsx            # the public component
├── EmbeddedCopilotRuntimeProvider.tsx  # AG-UI agent and event handling
├── workflow-chat-thread.tsx            # the thread UI (messages, composer, chips)
├── dataComponents.tsx                  # interactive cards (questions, pickers, Connect)
├── store.ts                            # zustand store holding the transcript
├── api.ts, constants.ts                # skeleton workflow and endpoint URLs
├── chat-config-context.tsx             # gives cards the JWT, base URL and environment
├── EmbeddedChatBoundary.tsx            # error boundary
└── assistant-ui/                       # markdown, tool card and reasoning renderers

That's deliberate. A chat is the most visible thing in your product, and you'll want it to look like your product, not like ours. The thread is built from assistant-ui primitives styled with Tailwind, so restyling it is ordinary React work. The dependencies are @ag-ui/client, @assistant-ui/react, @assistant-ui/react-markdown, zustand, lucide-react, tailwind-merge, and @bytechef/embedded for the ConnectDialog.

What Happens on Mount: The Skeleton Workflow

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

export const SKELETON_WORKFLOW_DEFINITION = JSON.stringify({
    description: '',
    inputs: [],
    label: 'New Workflow',
    tasks: [],
    triggers: [],
});

const response = await fetch(`${baseUrl}/api/embedded/v1/automation/workflows`, {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${jwtToken}`,
        'Content-Type': 'application/json',
        'X-Environment': environment,
    },
    body: JSON.stringify({definition: SKELETON_WORKFLOW_DEFINITION}),
});

const workflowUuid = (await response.text()).replace(/"/g, '').trim();

This is the same create endpoint the custom builder uses, and the workflow lands in the same place: the connected user's personal project for that environment, which ByteChef creates the first time it's needed. While the request is in flight, the component shows Starting.... If it fails (an expired JWT, a wrong baseUrl), it shows the error in red instead of the chat.

The UUID it gets back anchors the whole conversation. Every turn is sent to that workflow, and the agent reads and rewrites that workflow's definition. A ref latch makes sure only one skeleton is created, even under React StrictMode, which runs effects twice in development.

One consequence to plan for: every visit to the chat page creates a draft workflow, even if the user leaves without typing. We'll come back to that in Hardening.

What Happens on Each Turn

When the user sends a message, the component posts the conversation to ByteChef's copilot endpoint using an AG-UI HttpAgent:

const agent = new HttpAgent({
    agentId: 'workflow_editor',
    url: `${baseUrl}/api/embedded/v1/automation/workflows/${workflowUuid}/copilot/chat`,
    headers: {
        Authorization: `Bearer ${jwtToken}`,
        'X-Environment': environment,
    },
    threadId: conversationId,
});

agent.setState(
    trimmedSystemPrompt
        ? {additionalSystemPrompt: trimmedSystemPrompt, mode: 'BUILD', workflowUuid}
        : {mode: 'BUILD', workflowUuid}
);

await agent.runAgent({runId: randomUUID()}, subscriber);

Of everything in that state, the server only uses the system prompt. It ignores agentId and mode too: the endpoint always runs the workflow-editor build agent and rebuilds the rest of the state itself, as described in What the Agent Can and Can't Do.

AG-UI is an open protocol for streaming agent runs to a front-end. The response is a text/event-stream, and each event says what the agent is doing right now. The subscriber turns those events into chat UI:

AG-UI eventWhat the user sees
TEXT_MESSAGE_START, _CONTENT, _ENDA new assistant message, streamed in token by token
TOOL_CALL_START, _ARGS, _ENDA tool card named after the tool (for example searchTasks or updateWorkflow), with its input arguments filling in live
TOOL_CALL_RESULTThe tool card gets its result and a success or error icon. If the result is an interactive request, a card is added under it (see the next section)
RUN_ERRORA red error callout in the thread, such as The copilot run failed. Reference: 3f2b...
RUN_FINISHEDThe turn is over. The first time this happens, onWorkflowReady fires

Consecutive tool calls are grouped into a single assistant message so their cards stack neatly instead of each one taking a full message's spacing.

Errors tell the user what they can act on, and nothing more. When an embedded run fails for a reason the user can do something about, ByteChef sends a fixed sentence followed by the run's ID:

CauseWhat the user reads
No AI provider set up for the environmentNo AI provider is set up for this environment, so the chat can't answer.
The provider is rate limitingThe AI provider is rate limiting requests right now. Try again in a minute.
The provider is overloadedThe AI provider is overloaded right now. Try again in a minute.
The provider rejected its API keyThe AI provider rejected its API key. Ask an administrator to check it.
The conversation outgrew the modelThis conversation is too long for the AI model. Start a new chat.

Each one ends with "Reference: " and the run ID. Anything else reads "The copilot run failed. Reference: ...". The sentences are fixed text, never the exception's own message, because the browser is the user's and an exception can name tables, internal hosts or keys. The full cause goes to the server log under the same run ID, so when a user reports a failure, ask for the reference and search your ByteChef logs for it.

The thread also has the controls users expect from a modern chat:

  • Stop (the square button while a turn is running) aborts the run.
  • Edit on a user message rewinds the thread to that point and runs it again with the new text.
  • Refresh on the last assistant message regenerates it.

Edit and Refresh rewind the agent too, not just the screen. The agent's memory of the conversation lives on the server, keyed to the user, the workflow and the conversation. Every turn, the chat sends the history the user can see, and before the model runs ByteChef cuts its memory back to the last user message both sides agree on, so a reply the user edited away is gone for the agent as well. The server only ever removes messages this way; it never takes the client's word for what was said.

  • Copy and Export as Markdown under assistant messages.

Interactive Cards: When the Agent Needs the User

A text-only chat would force the agent to ask "which channel? please type its exact name" and hope for the best. Instead, some of the agent's tools return a small JSON envelope with a kind, and the chat renders a purpose-built card for it. Three of them (questions, the connection picker and Connect) do nothing on the server but hand the question to the browser. The property picker does a little more: it looks up the property's real options on the server first, using a connection the user is allowed to use.

{
    "kind": "create-connection",
    "componentName": "slack",
    "componentLabel": "Slack"
}
Embedded chat after the first prompt: a stack of tool cards (searchTasks, getTaskDefinition, listTasks, getTriggerDefinition, listConnectionsForComponent, askUserQuestion) followed by a Question 1 of 2 card asking which timezone the 9am schedule should run in

The sample maps four kinds to cards in dataComponents.tsx:

kindCardWhat happens on click
ask-user-questionOne or more multiple-choice questions, shown one at a time with Question 1 of 3 and a Previous linkThe answers are sent back as "User picked: ..."
select-property-optionA searchable list of valid values for a property, such as Slack channels or Google SheetsThe chosen label and its real value are sent back, and the agent writes the value into the workflow
select-connectionA searchable list of the user's existing connections for an appThe chosen connection's name and ID are sent back. With exactly one connection, it's picked automatically
create-connectionA Connect Slack buttonOpens the ConnectDialog. When the user finishes, the card changes to Connected Slack

When the agent shows a card, its turn ends right there: it doesn't guess the answer and carry on, it waits. The user's pick goes back as an ordinary message ("User picked: general"), which starts the next turn with exactly the information the agent asked for. When a card has been answered it collapses into a short summary with a green check (Picked: #standup), so the thread stays readable.

Embedded chat stopped at a searchable Slack channel picker listing random, general, testing, sales-team and more, under the selectPropertyOption tool card that produced it

Connect, Right in the Conversation

The create-connection card is where the whole series comes together. Say the user asks for a Slack reminder and hasn't connected Slack yet. The agent calls its createConnection tool, and the chat shows a Connect Slack button. Clicking it:

  1. Calls GET /api/embedded/v1/integrations with the user's JWT and finds the integration whose componentName is slack.
  2. Opens the ConnectDialog from part one for that integration, using useConnectDialog from @bytechef/embedded.
  3. Watches the dialog's portal, and when the dialog closes, marks the card as connected.

The user goes through the same OAuth or API-key flow they'd see on your Integrations page, without leaving the chat. When they come back, they tell the agent to continue, and it can now pick the new connection with select-connection.

Note that createConnection is signaling only. The agent never sees or stores credentials, and no connection is created on the server by the tool call. The browser drives the whole flow through the ConnectDialog, just as it would anywhere else in your app. If the app isn't one of your integrations, the card says No integration is available for Slack instead of opening anything.

Your House Rules: The System Prompt

The user decides what the workflow does. You get to decide how it's built. Open System prompt (optional) and write the rules every workflow from your product should follow:

Sample app New from Chat page with the System prompt disclosure open and the text Always notify the team in Slack, never by email. Keep workflows under five steps.

Good system prompts encode decisions your users shouldn't have to make:

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

A few details about how ByteChef treats it:

  • It's read on every turn. The component sends the current value with each run, so changing it mid-conversation affects the next turn.
  • It's trimmed and capped at 4,000 characters on the server.
  • It can't override the agent's core rules. ByteChef appends it to the agent's own system prompt under a heading that marks it as additional instructions provided by the integrating application, to be applied only where it doesn't conflict with the build rules, the workflow-definition contract, or safety constraints. Your rules can steer the agent; they can't turn it into something else.

In the sample, the system prompt is a text box on the page so you can experiment. In production, it usually belongs in your code, not in your users' hands: build it from your product's conventions, and maybe from what you know about the user (their plan, their time zone, the apps their company has approved).

What the Agent Can and Can't Do

The chat is a public surface: whatever runs in it runs with the connected user's JWT, and the browser controls the request body. So the copilot endpoint doesn't trust the AG-UI state it receives. On every turn, ByteChef:

  1. Checks the workflow belongs to the user. The {workflowUuid} in the path must be a workflow in the connected user's own project for that environment. Another user's UUID gets an error, not a conversation.
  2. Rebuilds the agent state from scratch. Everything the client sent in state is thrown away except the additional system prompt. The server then sets the values the run needs itself: BUILD mode, non-autonomous, the workflow ID it just verified, the user's project, the environment resolved from the X-Environment header (PRODUCTION if the header is missing), and a conversation key built from the tenant, the user and the workflow. A client can't switch the agent into another mode, point it at another workspace, or read another user's conversation by editing the request.
  3. Keeps the agent inside the user's own project. Every workflow tool checks that the workflow it touches belongs to that project, and project-level changes are off limits. If the model is talked into opening someone else's workflow, the tool refuses with "This chat can only work with the projects and workflows of its own project."; asked to create, rename, delete or publish a project, it gets "This chat cannot create, change, delete or publish projects."
  4. Lets the agent use only the user's own connections. The agent is only shown the connections the user owns or has been given. And whenever it binds a connection to a step or looks up options such as Slack channels, ByteChef checks the connection ID against that same list. Any other ID is refused with "This chat can only use connections its user is entitled to use.", so the agent can't borrow another customer's Slack.
  5. Limits the agent to your catalog. The agent can only use components that are either enabled integrations in that environment (the ones your users can connect from your Integrations page) or don't need a connection at all, such as schedules, conditions, loops and data mappers. Anything else doesn't show up in its searches, can't be looked up, and is rejected if it appears anywhere in a workflow the agent tries to save or validate, including inside conditions, loops and AI agent nodes.

That last point is easy to miss and important. If your product offers HubSpot and Slack but not Salesforce, a user asking for "Salesforce deals to Slack" gets a conversation about what's possible, not a workflow full of nodes that could never run:

Embedded chat asked to add an Airtable row for each new Salesforce opportunity: the agent explains Salesforce and Airtable aren't among the available components, suggests a webhook or a schedule with the HTTP client instead, and waits on a question card asking which approach to take

Handing Off the Result

The chat produces a real ByteChef workflow, so it isn't a dead end. When the first turn finishes, the component calls onWorkflowReady with the UUID it created on mount:

<EmbeddedWorkflowChat
    // ...
    onWorkflowReady={setWorkflowUuid}
/>

{workflowUuid && (
    <Button onClick={() => router.push(`/automations/${workflowUuid}`)}>Open workflow</Button>
)}

When the build is done, the agent says what it made and where:

Embedded chat summary of the finished workflow: Daily Standup Reminder, triggered every weekday at 9:00 AM America/New_York, posting to #general on Slack, with the existing Slack connection bound for test runs, followed by the run's tool cards

Two things about this callback are worth knowing:

  • It fires once, after the first turn, not when the workflow is "done". The agent might have built three steps in that turn, or it might only have asked a clarifying question. The UUID never changes, and the agent keeps updating the same workflow with every turn, so opening it later always shows the latest version. Treat the callback as "there's a workflow worth linking to now".
  • It's a callback, so you decide what it means. The sample shows an Open workflow button. You could also show a live preview next to the chat, enable a Save button, or record an analytics event.

The /automations/{uuid} page renders ByteChef's embedded editor, the same EmbeddedWorkflowBuilder we saw in part eight, so the user can review and fine-tune what they talked into existence. Chat to draft, canvas to polish.

From Draft to Running

Like a copied template, a chat-built workflow is a draft in the user's own project. Nothing runs until it's published and enabled, which is the right default for something an AI just wrote. The same calls from part seven take it live:

// Freeze the current definition as the user's first version
await fetchWithAuth(`/api/embedded/v1/automation/workflows/${workflowUuid}/publish`, {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({description: 'Created from chat'}),
});

// Turn it on (DELETE to turn it off)
await fetchWithAuth(`/api/embedded/v1/automation/workflows/${workflowUuid}/enable`, {method: 'POST'});

In the sample, the user does this themselves: Publish in the editor, then the toggle on the Automations list. A more guided product could add an Activate button under the chat that runs both calls once the user is happy.

Prerequisites on the ByteChef Side

The chat needs three things from your ByteChef instance:

  • The copilot enabled, with bytechef.ai.copilot.enabled=true. Without it the /copilot/chat endpoint doesn't exist and the first turn fails.
  • An AI provider configured, since the agent needs a model to run. The run's environment decides which provider settings apply.
  • Integrations enabled for the environment. As described above, the agent only builds with your enabled integrations plus connection-free components. An instance with no enabled integrations can still build schedules and logic, but nothing that talks to an app.

Embedded workflows and the copilot endpoint are Enterprise Edition features.

Hardening the Demo for Production

The sample page is intentionally small. Before you ship a chat, close these gaps:

  • Clean up abandoned drafts. Each visit creates a New Workflow draft, whether or not the user types. Either create the skeleton lazily (on the first message), or delete untouched drafts with DELETE /automation/workflows/{uuid} when the user leaves.
  • Write suggestions for building. The sample's chips ("Describe what this workflow does end-to-end", "Which properties of this node are required?") come from ByteChef's editor copilot, where a workflow already exists. On an empty workflow, chips like "Post a Slack message every Monday at 9:00" or "When a HubSpot deal closes, create a Jira ticket" show users what they can ask for.
  • Own the system prompt. Generate it in your code rather than exposing a text box to end users.
  • Pass environment explicitly. The component defaults to PRODUCTION. A JWT signed for another environment is rejected when the component creates its workflow, and the sample says so: "ByteChef rejected the chat's token for the DEVELOPMENT environment. Check that the token is valid and was signed for the same environment as the component's environment prop."
  • Decide what "ready" means. onWorkflowReady fires after the first turn. If you want a stricter signal (a trigger exists, no open issues), fetch the workflow when the callback fires and check it yourself.
  • Support multi-select questions. The sample's question card renders every question as single-select; the multiSelect flag the agent sends isn't honored yet.
  • Watch the error boundary. EmbeddedChatBoundary hides a harmless assistant-ui unmount error, but it also renders nothing for any other crash. Replace that null with a friendly fallback and report the error.
  • Offer a way back. Users expect a New chat button. Unmount and remount the component (for example by changing its key) to start fresh with a new skeleton workflow and a new conversation, which also gives the agent an empty memory.
  • Surface the error reference. Every failed run ends with a run ID. Make it easy to copy, and tell your support team to search the ByteChef logs for it.

What You Own, What ByteChef Owns

Chat moves the line further toward ByteChef than any part so far, and leaves you the parts that make it feel like your product:

  • You own where the chat appears, how it looks (the component is your source code), the suggestions, the system prompt and the conventions it encodes, which integrations exist and are enabled, and what happens when a workflow is ready.
  • ByteChef owns the agent and its tools, the AG-UI stream, component search and property lookups, validation of what the agent writes, the per-user project and workflow storage, the security boundary around every turn, the ConnectDialog and credential storage, and the engine that runs the result.

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

Nine Parts In

Here's the series so far:

  1. Connect your users' apps (the JWT and ConnectDialog everything rests on).
  2. Act on those connected apps directly with the ComponentKit.
  3. Chat with an AI assistant that uses the ComponentKit's tools.
  4. Trigger their workflows from your product's events.
  5. Call a workflow like a synchronous API.
  6. Agent over MCP: the same tools, discovered through an open protocol.
  7. Author workflows in a builder you designed yourself.
  8. Start from a template you curated.
  9. Talk a workflow into existence with ByteChef's agent (this part).

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

Following along? Enable the copilot and an AI provider in ByteChef, run the embedded sample app, and open Automations → New from Chat. Ask it for something small, like a weekday Slack reminder, and watch it build.

Subscribe to the ByteChef Newsletter

Get the latest guides on complex automation, AI agents, and visual workflow best practices delivered to your inbox.

Related posts