AI-powered applications need to be responsive and update in real-time to keep users engaged. GenSX provides utilities and hooks that make it easy to build interactive, streaming applications on top of your workflows. These capabilities include:
publishObject and consume it with the useObject hookpublishEvent and consume them with the useEvents hookThis guide will walk you through how to build responsive UI’s that update in real-time as your workflow runs. It covers both how to stream data from your workflow and then how to consume it in your React app.
There are multiple ways to stream data from your workflows. You can stream outputs, objects, events, or any arbitrary data.
GenSX provides a publishObject function that allows you to stream an object to the client. It’s designed for you to continually publish the latest state of an object as it gets updated. The changes will be patched to efficiently update the state on the client.
import * as gensx from "@gensx/core";
interface Message {
role: "user" | "assistant" | "system";
content: string;
}
gensx.publishObject<Message[]>("messages", [
{ role: "assistant", content: "Hello, how can I help you today?" },
]);You can also use createObjectStream to create a reusable function for publishing a given object.
const publishMessages = gensx.createObjectStream<Message[]>("messages");
publishMessages([
{ role: "assistant", content: "Hello, how can I help you today?" },
]);On the client side, you use the useObject hook to subscribe to the object and get the latest state.
GenSX provides a publishEvent function that allows you to stream events to the client. It’s designed for you to publish events that happen over time.
interface ProgressEvent {
progress: "brainstorming" | "researching" | "writing" | "editing";
}
gensx.publishEvent<ProgressEvent>("progress", {
progress: "brainstorming",
});There is also a createEventStream helper available for creating a reusable function for publishing the events.
On the client side, you use the useEvents hook to subscribe to the events and get a list of events or process them in the onEvent callback.
GenSX also provides a lower-level publishData function that allows you to pass arbitrary data to the client. It’s designed for you to pass data that doesn’t fit into the other categories.
interface Answer {
answer: string;
confidence: number;
}
gensx.publishData<Answer>({
answer: "42",
confidence: 0.95,
});Unlike publishEvent and publishObject, publishData does not take in a label and does not have a corresponding react hook so it will need to be consumed manually. The event will have type: "data" and the data will be available under the data property.
{
"type": "data",
"data": {
"answer": "42",
"confidence": 0.95
},
"id": "1752110129329",
"timestamp": "2025-07-10T01:15:29.329Z"
}Often we recommend using the utilities above to stream and then just returning the final accumulated result from your workflow. That approach makes consuming the stream simpler and makes it easy to read the outputs in traces. However, GenSX gives you the flexibility to stream the output directly too.
To stream an output, just have your workflow return a ReadableStream or an AsyncIterator. Here’s an example of streaming a chat response with the Vercel AI SDK:
export const StreamingChat = gensx.Workflow(
"StreamingChat",
({ prompt }: { prompt: string }) => {
const result = streamText({
messages: [
{
role: "system",
content:
"you are a trash eating infrastructure engineer embodied as a racoon. Be sassy and fun. ",
},
{
role: "user",
content: prompt,
},
],
model: openai("gpt-4o-mini"),
});
return result.textStream;
},
);When using the streaming headers, the output will be a series of events with the type: "output" with the data in the content property:
{
"id": "1752109017087",
"timestamp": "2025-07-10T00:56:57.087Z",
"type": "output",
// content is a string, JSON will be stringified
"content": "Hello, world!"
}The @gensx/react library is the best way to consume streaming data from GenSX workflows.
To avoid exposing your GenSX API key to the client, we recommend setting up a passthrough API that forwards the request to the GenSX API. For brevity, we won’t include the code in this guide but you can grab the code here as a reference.
useWorkflow hookThe useWorkflow hook lets you run a workflow and subscribe to its events and output.
const { inProgress, error, output, execution, run, stop, clear } = useWorkflow<
ChatInput, // the input type of the workflow
ChatOutput // the output type of the workflow
>({
config: {
baseUrl: "/api/gensx/chat", // the passthrough API route
},
});
// Run the workflow
await run({
inputs: {
userMessage: "Hello, how are you?",
},
});useWorkflow also supports callbacks for onStart, onComplete, onError, and onEvent that you can use to handle the workflow’s lifecycle.
const { error, output, execution, run } = useWorkflow<ChatInput, ChatOutput>({
config: {
baseUrl: "/api/gensx/chat", // the passthrough API route
},
onStart: () => {
console.log("Workflow started");
},
onComplete: () => {
console.log("Workflow completed");
},
onError: (error) => {
console.error(error);
},
onEvent: (event) => {
if (event.type === "data") {
console.log(event.data);
} else if (event.type === "event") {
console.log(event.label);
console.log(event.data);
} else if (event.type === "output") {
console.log(event.content);
}
},
});useObject hookThe useObject hook lets you subscribe to an object published via publishObject and get its latest state.
const messages = useObject<Message[]>(execution, "messages");Whenever a new version of the object is published, the value will automatically be updated, making it a great way to render data in real-time. In this example, you can render the messages as they are published and the latest text will be streamed to the UI.
messages.map((message, index) => (
<ChatMessage key={index} role={message.role} content={message.content} />
));useEvents hookThe useEvents hook lets you subscribe to events and get the latest state.
const progressEvents = useEvents<ProgressEvent>(execution, "progress");
progressEvents.forEach((event) => {
console.log(event.progress);
});You can also pass a callback function to the hook to process the events as they are received.
const progressEvents = useEvents<ProgressEvent>(
execution,
"progress",
(event) => {
setState(event.progress);
},
);In addition to the events created by publishEvent, publishObject, and publishData, GenSX also emits the following events:
| Event Type | Description |
|---|---|
start | Emitted when the workflow starts |
end | Emitted when the workflow ends |
component-start | Emitted when a component starts |
component-end | Emitted when a component ends |
output | Emitted when an output is returned |
error | Emitted when an error occurs |
// start event
{
"type": "start",
"workflowName": "Chat",
"id": "1752108242902",
"timestamp": "2025-07-10T00:44:02.902Z"
}
// end event
{
"type": "end",
"id": "1752108243493",
"timestamp": "2025-07-10T00:44:03.493Z"
}
// component-start event
{
"type": "component-start",
"componentName": "StreamText",
"componentId": "StreamText:7e1339d69eee8d3d",
"id": "1752108242902",
"timestamp": "2025-07-10T00:44:02.902Z"
}
// component-end event
{
"type": "component-end",
"componentName": "StreamText",
"componentId": "StreamText:7e1339d69eee8d3d",
"id": "1752108242904",
"timestamp": "2025-07-10T00:44:02.904Z"
}
// output event
{
"id": "1752109017087",
"timestamp": "2025-07-10T00:56:57.087Z",
"type": "output",
// content is a string, JSON will be stringified
"content": "{\"message\":\"Hello, world!\"}"
}
// error event
{
"id": "1752109017087",
"timestamp": "2025-07-10T00:56:57.087Z",
"type": "error",
"error": "An error occurred"
}To consume the streaming messages from the API, you need to set the Accept header to text/event-stream or application/x-ndjson. The @gensx/react and @gensx/client libraries automatically set the Accept header for you. If you don’t set the Accept header, only outputs will be streamed and they will be returned as a basic application/stream.
GenSX also allows you to resume a stream at any time by calling the progress API with the lastEventId query parameter:
curl "https://api.gensx.com/org/{orgName}/workflowExecutions/{executionId}/progress?lastEventId={lastEventId}" \
-H "Authorization: Bearer {apiKey}" \
-H "Accept: text/event-stream" # or application/x-ndjsonOptionally, you can emit the lastEventId and the entire stream will be replayed.
The links below are end-to-end examples showing how to build streaming applications with GenSX: