Skip to Content
Basic Concepts

Basic concepts

GenSX is a simple typescript framework for building complex LLM applications. It’s built around functional, reusable components that are composed using JSX to create and orchestrate workflows.

Components

Components are the building blocks of GenSX applications; they’re pure TypeScript functions that:

  • Accept props as input
  • Produce an output that can be used by other components or returned as the result of the workflow
  • Don’t depend on global state
  • Are strongly typed using TypeScript

When you define a component, you need to include four things:

  1. The input type, or props, for the component
  2. The output type for the component
  3. The name of the component that’s used for telemetry, tracing, visualization, and caching
  4. The function that produces the component’s output

Here’s an example of a simple component:

interface GreetUserProps { name: string; } type GreetUserOutput = string; const GreetUser = gensx.Component<GreetUserProps, GreetUserOutput>( "GreetUser", async ({ name }) => { return `Hello, ${name}!`; }, );

Components can return data as well as other components. Here’s an example of a component that returns another component:

const GreetUser = gensx.Component<GreetUserProps, GreetUserOutput>( "GreetUser", async ({ name }) => { return ( <ChatCompletion model="gpt-4o-mini" messages={[ { role: "system", content: "You are a friendly assistant that greets people warmly.", }, { role: "user", content: `Write a greeting for ${name}.` }, ]} /> ); }, );

Components can also return arrays and structured objects containing other components. The pattern also works with array operations like map for processing items in parallel.

const AnalyzePosts = gensx.Component<AnalyzePostsProps, AnalyzePostsOutput>( "AnalyzePosts", ({ posts }) => ({ analyses: posts.map((post) => ({ summary: <PostSummarizer post={post} />, commentAnalysis: ( <CommentsAnalyzer postId={post.id} comments={post.comments} /> ), })), }), );

In this example, AnalyzePosts will produce an array of objects that contains the summaries and commentAnalysis for each post. GenSX handles resolving all of the nested components and collects them into a final value for you.

JSX and data flow

GenSX uses JSX to compose components into workflows. The output of a parent component can be passed to a child component through a child function. This pattern enables you to create chains of components where each step’s output feeds into the next component’s input.

When components are combined, they will return the value of the innermost component.

// Parent receives output from ChildComponent through the child function <ParentComponent input="some data"> {(parentResult) => <ChildComponent data={parentResult} />} </ParentComponent>

Unlike React’s concurrent rendering model, GenSX evaluates your workflow as a dependency graph. Components execute in parallel whenever possible, while automatically ensuring that all required inputs are available before a component starts executing.

While GenSX uses a tree-based structure with JSX, it still supports all the capabilities you’d expect from graph-based workflows including representing cyclic and agentic patterns.

Component types

GenSX provides two main types of components:

  1. Components (gensx.Component) - Components that produce an output value and can handle both synchronous and asynchronous operations
  2. Streaming Components (gensx.StreamComponent) - Components designed to handle responses from LLMs, working in both streaming and non-streaming contexts

The power of a StreamComponent is that a single implementation can be used interchangeably in both streaming and non-streaming contexts without any code changes - you just toggle the stream prop:

const stream = await gensx.execute<Streamable>( <MyStreamingComponent input="some data" stream={true} />, ); // Process the streaming response for await (const chunk of stream) { process.stdout.write(chunk); }

Running workflows

Workflows and components are synonymous in GenSX. You’ll often design workflows that are composed of multiple components, but they can also just be a single component. Regardless, you’ll need to define a top level component that will be used to run the workflow. You can then use gensx.Workflow().run(props) to run the workflow:

const result = await gensx .Workflow("MyWorkflow", MyComponent) .run({ input: "some data" });

When components are run via a workflow, GenSX processes the JSX tree from top to bottom while executing components in parallel wherever possible.

Because workflows are just components, you can run and evaluate them in isolation, making it easy to debug and verify individual steps of your workflow. This is particularly valuable when building complex LLM applications that need robust evaluation.

Rather than having to run an entire workflow to test a change to a single component, you can test just that component, dramatically speeding up your dev loop. This isolation also makes unit testing more manageable, as you can create specific test cases without having to worry about the rest of the workflow.

Executing sub-workflows

In some cases, you may need to run a sub-workflow within a larger workflow. You can do this by using gensx.execute():

const result = await gensx.execute(<MyComponent input="some data" />);

This allows you to work with a component in TypeScript without having to drop into the JSX layer. When used inside a component, gensx.execute() preserves the current context and maintains the component hierarchy so it will integrate naturally with the rest of the workflow.

Sub-workflows are especially useful for more complex operations like map/reduce:

const AnalyzeReviews = gensx.Component< AnalyzeReviewsProps, AnalyzeReviewsOutput >("AnalyzeReviews", async ({ reviews }) => { // Map: Extract topics from each review in parallel const topics = await gensx.execute<GetTopicOutput[]>( reviews.map((r) => <GetTopic review={r} />), ); // Reduce: Count frequency of each topic return topics.reduce( (counts, topic) => ({ ...counts, [topic]: (counts[topic] || 0) + 1 }), {}, ); });

Contexts

Contexts provide a way to share data across components without explicitly passing them through props. They’re similar to React’s Context API but adapted for GenSX workflows. Contexts are particularly useful for:

  • Sharing configuration across multiple components
  • Providing dependencies to deeply nested components
  • Managing state that needs to be accessed by multiple components

Here’s how to create and use a context:

interface GreetUserProps { name: string; } // Create a context with a default value const UserContext = gensx.createContext({ name: "" }); // Use the context in a component const GreetUser = gensx.Component<{}, string>("GreetUser", () => { const user = gensx.useContext(UserContext); return `Hello, ${user.name}!`; }); // Provide a value to the context const ContextExample = gensx.Component<{}, string>("ContextExample", () => ( <UserContext.Provider value={{ name: "John" }}> <GreetUser /> </UserContext.Provider> ));

Contexts are a useful way to pass configuration without prop drilling. However, they do make your components less reusable so we recommend that you use them sparingly.

For more details on providers, see the Context and Providers page.

Providers

Providers are a specialized use of contexts that focus on managing configuration and dependencies for your workflow. They’re built using the context system described above, but provide a more focused API for configuration management.

The main provider available today is the OpenAIProvider, which manages your OpenAI API key:

const BasicChat = gensx.Component<BasicChatProps, string>( "BasicChat", async ({ prompt }) => { return ( <OpenAIProvider apiKey={process.env.OPENAI_API_KEY}> <ChatCompletion model="gpt-4o-mini" messages={[{ role: "user", content: prompt }]} /> </OpenAIProvider> ); }, );

For more details on providers, see the Context and Providers page.

Last updated on