Beyond Single-Output: Building Multi-Alternative Steps in Our Workflow Engine
We tackled a common challenge in AI-powered workflows: the need for design alternatives. Here's how we engineered a multi-output step selection system, pausing for user choice, across our entire stack.
Building a workflow engine is a journey of anticipating user needs, especially when integrating the unpredictable yet powerful nature of Large Language Models (LLMs). One of the most common pieces of feedback we've received, and a challenge we've felt ourselves, is the "one-and-done" nature of LLM outputs. While powerful, the first generation isn't always the best generation, and often, users want to explore alternatives before committing to a path.
That's why our latest development sprint focused on a critical enhancement: multi-output step selection. Imagine a workflow where a step, instead of just producing one result, generates several distinct alternatives, pauses, and lets the user pick the best one to continue the pipeline. This isn't just a feature; it's a paradigm shift for creative and iterative workflows.
This post dives into the technical journey of bringing this vision to life, from database schema to user interface, including the inevitable bumps and valuable lessons learned along the way.
The Challenge: Embracing Variability
Our goal was clear: implement a system where a workflow step could generate N alternative outputs, present them to the user, and then proceed with the chosen alternative. This required a full-stack overhaul, touching every layer of our application.
The core idea was to empower users. When an LLM step like "Design Features" runs, it might generate three distinct approaches. Instead of forcing the user to regenerate or manually edit, we now offer them as a curated selection, complete with previews and context.
A Full-Stack Symphony: From Schema to UI
The implementation spanned all six layers of our application: the database schema, the workflow engine's core logic, our tRPC router, shared constants, the workflow builder UI, and the execution UI.
1. Data Model: Storing the Choices
First, we needed to update our WorkflowStep model in prisma/schema.prisma to accommodate the new state:
model WorkflowStep {
// ... other fields
generateCount Int? // How many alternatives to generate (if applicable)
alternatives Json? // Array of generated alternatives (JSON blob)
selectedIndex Int? // Index of the user's chosen alternative
// ...
}
generateCount: An optional integer to specify how many variations an LLM step should produce.alternatives: AJsonfield to store the array of generated outputs. This is flexible, allowing us to store not just the content but also metadata like token count, estimated cost, and a label.selectedIndex: Once a user picks, this field records which alternative was chosen.
2. The Engine's New Rhythm: Generate, Pause, Resume
The runWorkflow() function, the heart of our engine, underwent significant changes.
- Variation Strategies: We introduced
VARIATION_STRATEGIESconstants (e.g.,Balanced,Conservative,Ambitious). These aren't just labels; they come with temperature offsets and prompt suffixes that subtly guide the LLM to produce diverse yet relevant outputs. WhengenerateCountis specified, the engine now iteratesNtimes, callingexecuteStep()for each variation, applying a different strategy, and storing the results. - Pausing for Selection: After generating all alternatives, the workflow pauses, changing its event type to
"alternatives_ready". - Resume Handling: The engine now checks if a step has
alternativesand aselectedIndex. If so, it uses the content of the selected alternative as the step's final output, ensuring downstream steps receive the correct data via our{{steps.StepName.content}}templating. - Resume Guard: A crucial addition: we block workflow resumption if the current step has unselected alternatives, preventing accidental progression without user input.
3. API & Router: The selectAlternative Mutation
Our src/server/trpc/routers/workflows.ts file gained a new mutation: selectAlternative. This mutation:
- Validates user ownership and ensures the
selectedIndexis within bounds. - Updates the
WorkflowStepwith the chosenselectedIndex. - Sets the step's final
outputbased on the selected alternative's content.
This mutation is the bridge between the user's choice in the UI and the engine's ability to resume.
4. UI/UX: Making Choices Intuitive
The most visible changes landed in our Next.js frontend, enhancing both the workflow builder and the execution view.
- Builder Configuration: In the workflow builder, we added
generateCount?to ourStepTemplateinterface and ensured it flows through ourcreateandduplicatemutations. For specific LLM steps like "Extension Features" and "Features," we pre-setgenerateCount: 3as a sensible default. - Execution UI - Alternative Selection Panel:
- Toggle Buttons: For LLM steps, a simple
1x/2x/3xtoggle now appears in theSortableStepCardheader, allowing users to dynamically adjustgenerateCount. [Nx]Badge: A clear[3x]badge in the step header immediately indicates a multi-output step.- Radio-style Cards: When alternatives are generated, a dedicated selection panel appears. Each alternative is presented as a card, showing a
label, estimatedtokens,cost, and apreviewof its content. These are radio-style, making selection intuitive. - Sticky "Continue" Button: A prominent, sticky "Continue with [Label]" button appears, chaining the
selectAlternativemutation with the workflowresumemutation for a seamless experience. - Post-selection "Other Alternatives": After selection, the unchosen alternatives collapse into a "Other alternatives" section, still accessible with copy buttons for reference.
- Toggle Buttons: For LLM steps, a simple
5. Enhancing Prompt Visibility & Control
Beyond the multi-output feature, we also rolled out significant improvements to how users interact with prompts:
- Collapsible PROMPT + system Section: Every step now features a collapsible section displaying both the system and user prompts in monospace blocks, offering full transparency into the LLM's instructions.
- Inline Prompt Editing: Users can now directly edit the system and user prompts within these sections using textareas. A
steps.updatemutation handles saving these changes, allowing for dynamic prompt iteration mid-workflow. This is a game-changer for debugging and fine-tuning.
Lessons Learned: Navigating the Technical Minefield
No significant feature implementation is without its challenges. Here are a few "gotchas" we encountered and how we overcame them:
Lesson 1: Prisma's Json Type and TypeScript Type Safety
- Problem: We tried to pass an array of
Record<string, unknown>(ouralternativesarray) directly to a Prisma update operation for aJsonfield. TypeScript rightfully complained:Record<string, unknown>[]is not assignable toPrisma.InputJsonValue. - Attempted Solution: Initially, we tried various type assertions, but the core issue was understanding what
InputJsonValuetruly expects. - Workaround/Solution: The
Jsontype in Prisma's schema is flexible, but its TypeScript representationPrisma.InputJsonValueis stricter. The simplest, cleanest solution was to explicitly cast the array:alternatives as unknown as Prisma.InputJsonValue. This, combined with importingPrismafrom@prisma/client, resolved the TS error and allowed the data to be stored correctly. - Takeaway: When dealing with Prisma's
Jsontype, remember that while it's schema-flexible, TypeScript requires explicit handling.unknown as Prisma.InputJsonValueis often the necessary bridge.
Lesson 2: The Stale Prisma Client
- Problem: After adding
generateCountto ourWorkflowStepmodel, subsequent attempts to create new workflows via our tRPC API failed with anUnknown argument 'generateCount'error. - Attempted Solution: We double-checked the schema, the API input, everything seemed correct.
- Workaround/Solution: The issue wasn't in our code but in the generated Prisma client. It was "stale" from a previous session and hadn't picked up the new field. A quick
npx prisma generatefollowed by a dev server restart brought everything back into sync. - Takeaway: Always remember to regenerate your Prisma client (
npx prisma generate) after any schema changes, and restart your development server to ensure the updated client is loaded. This is a common pitfall!
Lesson 3: Playwright Scrolling in Confined Containers
- Problem: While writing Playwright tests for the new UI, we tried to take full-page screenshots on a mobile viewport using
window.scrollTo(). The scroll didn't seem to take effect, leading to clipped screenshots. - Attempted Solution: Various
window.scrollTocombinations, waiting for scroll, etc. - Workaround/Solution: Our dashboard uses a custom scrollable container, not the
windowitself, for its main content area.window.scrollTo()only affects the browser window's scrollbar. The solution involved:- Using
fullPage: truefor screenshots where possible to capture the entire render tree regardless of scroll. - For elements that needed to be in view, we simulated user interaction by clicking expansion buttons to reveal hidden content, instead of programmatic scrolling.
- Using
- Takeaway: Be mindful of custom scrollable containers in your application.
window.scrollTo()won't work if your content scrolls within adivwithoverflow: auto. Target the specific container or simulate user interaction to bring elements into view for testing.
What's Next?
With the core multi-alternative system in place, our immediate next steps involve thorough end-to-end testing and some exciting future enhancements:
- Comprehensive Testing: Verify the entire flow: creation, generation of alternatives, selection on both desktop and mobile, and correct content propagation to downstream steps.
- Prompt Editing on Completed Steps: Consider adding the prompt edit button to completed steps, enabling re-run scenarios with modified instructions.
- Accurate Cost Estimation: Our
estimateWorkflowCostfunction currently doesn't account for thegenerateCountmultiplier. Updating this will provide more accurate cost projections for users.
This sprint has been a testament to the power of full-stack development. By integrating database, backend logic, and a responsive UI, we've delivered a feature that significantly enhances the flexibility and user experience of our LLM-powered workflows. The ability to explore, select, and refine outputs is no longer a workaround but a core part of our engine.
{"thingsDone":[
"Implemented multi-output step selection (design alternatives) for workflow engine.",
"Added generateCount, alternatives, selectedIndex fields to WorkflowStep model (Prisma).",
"Introduced VARIATION_STRATEGIES for diverse LLM outputs.",
"Extended WorkflowEvent type for 'alternatives_generating' and 'alternatives_ready'.",
"Developed alternatives generation loop in runWorkflow() with strategy-modified prompts.",
"Added resume handling for selected alternatives.",
"Created selectAlternative tRPC mutation for user selection.",
"Implemented resume guard to block progression on unselected alternatives.",
"Updated create/duplicate mutations to pass generateCount.",
"Added generateCount to StepTemplate and StepConfig interfaces/helpers.",
"Integrated 1x/2x/3x toggle buttons in SortableStepCard for LLM steps.",
"Developed alternatives selection panel with radio-style cards, previews, and metadata.",
"Added sticky 'Continue with [Label]' button for seamless selection and resume.",
"Included post-selection 'Other alternatives' collapsible section.",
"Added [Nx] badge in step headers for multi-output steps.",
"Implemented collapsible PROMPT + system section per step.",
"Enabled inline prompt editing (system/user prompts) with save/cancel functionality.",
"Fixed Prisma client regeneration issues.",
"Resolved TypeScript errors related to Prisma.InputJsonValue and default configs."
],"pains":[
"TypeScript error when assigning Record<string, unknown>[] to Prisma.InputJsonValue.",
"Stale Prisma client causing 'Unknown argument' errors after schema changes.",
"Playwright window.scrollTo() failing in custom scrollable containers."
],"successes":[
"Successfully implemented a complex full-stack feature across 6 architectural layers.",
"Improved user experience by allowing choice and control over LLM outputs.",
"Enhanced developer and user transparency with inline prompt viewing and editing.",
"Overcame key technical hurdles with Prisma typing and client synchronization.",
"Delivered a robust foundation for future iterative AI workflows."
],"techStack":[
"Prisma (ORM)",
"PostgreSQL (Database)",
"TypeScript (Language)",
"tRPC (API Layer)",
"Next.js (Frontend Framework)",
"React (UI Library)",
"Playwright (Testing)"
]}