Empowering AI Workflows: Granular Control with Per-Step Provider & Persona Overrides
We just shipped a major update to our AI workflow engine, giving users unprecedented control over provider selection and persona behavior at a per-step level. Say goodbye to workflow failures caused by external API issues!
Ever found your carefully crafted AI workflow grinding to a halt because a third-party provider decided to throw a billing tantrum? Or perhaps you needed to fine-tune the personality of an AI model for just one specific step, without altering the entire workflow's persona? We've been there, and we just shipped a major update to tackle these exact challenges head-on.
The Problem: When External Dependencies Let You Down
Imagine running a critical multi-step AI workflow, only to have it fail mid-way because your chosen LLM provider is experiencing an outage or, worse, a billing hiccup (looking at you, Anthropic!). Previously, your only recourse might have been to duplicate the entire workflow, switch the provider globally, and re-run from scratch. This is inefficient, frustrating, and a huge productivity killer.
Similarly, while workflow-level personas are fantastic for consistency, sometimes a single step demands a slightly different tone or focus – perhaps a more critical reviewer persona for a specific output, or a more creative one for brainstorming. Our users needed more surgical precision.
The Solution: Granular Control at Your Fingertips
Our latest update introduces per-step provider/model selection and persona overrides, bringing unprecedented flexibility and resilience to your AI workflows.
1. On-the-Fly Provider Switching
Now, if a step fails due to provider issues, you can simply click on the step, select an alternative provider (e.g., switch from Anthropic to OpenAI or Google), and re-run that specific step or the remainder of the workflow. No more re-creating workflows, no more lost progress. It's about empowering you to keep your workflows running smoothly, even when external services falter.
2. Persona Pinpointing
Beyond just switching providers, you can now assign a specific persona to individual workflow steps. This means you can have a general "friendly assistant" persona for most steps, but then inject a "critical editor" persona for a review step, or a "creative brainstormer" for an idea generation phase. This level of control allows for incredibly nuanced and effective workflow designs, ensuring each LLM interaction is perfectly aligned with its purpose.
Under the Hood: A Peek at the Implementation
Bringing this feature to life involved touching several layers of our stack, from the database schema to the frontend UI.
Database Schema Evolution (Prisma)
We extended our WorkflowStep model in prisma/schema.prisma to include an optional personaId (a UUID) and established a reverse relation on the Persona model. This allows a persona to "know" which steps are using it, and a step to optionally "know" which persona it's overriding with. After a quick npm run db:push && npm run db:generate, our database and Prisma client were ready for the new data.
// Relevant snippets from prisma/schema.prisma
model WorkflowStep {
// ... other fields
personaId String? @db.Uuid
persona Persona? @relation(fields: [personaId], references: [id])
// ...
}
model Persona {
// ... other fields
workflowSteps WorkflowStep[]
// ...
}
API & Business Logic (tRPC & TypeScript)
The steps.update mutation in our tRPC router (src/server/trpc/routers/workflows.ts) was updated to accept the new personaId. Crucially, our executeStep() function in the workflow-engine.ts now intelligently checks for a per-step persona override. If present, it takes precedence over any workflow-level persona, ensuring the LLM interaction is precisely tailored. We also ensured personaId is correctly carried through in our duplicate mutation, using Prisma's connect syntax for relational data.
// src/server/trpc/routers/workflows.ts (simplified)
export const workflowRouter = t.router({
steps: t.router({
update: protectedProcedure
.input(
z.object({
id: z.string().uuid(),
// ... other step fields
personaId: z.string().uuid().nullable().optional(), // New field!
})
)
.mutation(async ({ ctx, input }) => {
// ... logic to update WorkflowStep including personaId
}),
// ... duplicate mutation also handles personaId
}),
});
// src/server/services/workflow-engine.ts (simplified)
async function executeStep(step: WorkflowStep, workflow: Workflow, teamId: string) {
let personaToUse;
if (step.personaId) {
// Load per-step persona from DB
personaToUse = await prisma.persona.findUnique({ where: { id: step.personaId } });
} else {
// Fallback to workflow-level persona
personaToUse = await prisma.persona.findUnique({ where: { id: workflow.personaId } });
}
// Inject persona into LLM call
// ...
}
Frontend Experience (Next.js & React)
Integrating these controls into the UI required careful thought to ensure a seamless and intuitive user experience.
- Provider Picker: For
pendingorpausedworkflows, each step header now proudly displays a compactProviderPicker. This allows for quick, on-the-fly switching. Forrunningorcompletedworkflows, we maintain the existing read-only display for consistency, preventing accidental changes to historical data. - Persona Dropdown: Within the expanded body of each step, after the prompt editor, you'll find a new
<select>dropdown for persona selection. This provides a clear and intuitive way to assign a specific persona. - We also optimized our persona data loading, ensuring all available personas are loaded upfront to power this new dropdown efficiently.
And of course, npm run typecheck passes clean – a testament to our commitment to type safety!
Lessons Learned: The Nested Button Saga
Development isn't always a smooth sail. One particular challenge arose when integrating the ProviderPicker into the step header. Our initial thought was to place it directly inside the existing header <button> element that toggles step expansion.
The Problem: This quickly led to issues. HTML specification prohibits nesting interactive elements like buttons. Doing so creates invalid HTML and, more practically, causes unpredictable click propagation behavior. Clicks on the ProviderPicker (which itself contains buttons) would inadvertently trigger the parent step expansion button, leading to a frustrating user experience where expanding a step also tried to open the provider picker, or vice-versa.
The Solution: We restructured the step header. Instead of a single toggle button wrapping everything, we now have a <div> container. Inside this div, the toggle <button> sits on the left, and the ProviderPicker is housed in its own <div> on the right. To ensure the ProviderPicker's internal clicks don't bubble up and affect other elements, we applied onClick={(e) => e.stopPropagation()} to the picker's wrapper. This seemingly small change was critical for a robust and user-friendly interface. It's a classic example of how seemingly minor UI decisions can have significant underlying technical implications and how adhering to HTML best practices can save a lot of headaches!
// Simplified JSX structure for the step header
<div className="flex items-center justify-between">
{/* Step expansion toggle button */}
<button onClick={toggleStepExpansion}>
{step.name}
</button>
{/* ProviderPicker, now in its own div, preventing nested buttons */}
<div onClick={(e) => e.stopPropagation()}>
{workflow.status === 'pending' || workflow.status === 'paused' ? (
<ProviderPicker stepId={step.id} currentProvider={step.provider} />
) : (
<span>{step.provider}</span> // Read-only for running/completed
)}
</div>
</div>
What's Next?
With the feature fully implemented and type-checked, our immediate next steps involve thorough manual verification. We'll be testing scenarios like:
- Switching providers on failed steps and re-running to ensure successful recovery.
- Verifying per-step persona selection correctly persists and influences LLM output.
- Ensuring workflow-level personas still function as expected when no per-step override is present.
- Confirming completed and running workflows correctly show read-only provider text (no picker).
This new capability marks a significant leap forward in workflow resilience and user control. We're excited to see how it empowers you to build even more robust and sophisticated AI applications!