From Chaos to Cohesion: Crafting Multi-Item Workflows with TypeScript and tRPC
Dive into the journey of building a powerful feature: creating coordinated workflows from multiple action points, tackling everything from UI multi-select to cross-item dependency analysis and a tricky TypeScript Set iteration gotcha.
Orchestrating Complexity: Our Latest Workflow Evolution
Just wrapped up a significant chunk of work that feels like a real step forward in how our platform helps users manage their tasks: implementing group workflow creation for action points. For a while now, users could generate AI-powered workflows for individual action points. But what about when you have a cluster of related tasks, perhaps from a single project, that need a coordinated approach? That's where this new feature shines.
The goal was ambitious: allow users to multi-select several action points, then generate a single, cohesive workflow that understands their interdependencies, assigns appropriate personas to individual steps, and culminates in a synthesis review. It's like turning a pile of scattered LEGOs into a beautifully engineered model with a single click.
I'm happy to report that the core feature is now complete, committed (b2aafe9), and pushed to origin/main. All tests are passing, and type checks are clean. Let's break down how we got there.
The Brains Behind the Operation: group-prompt-builder.ts
The heart of this new functionality lies in src/server/services/group-prompt-builder.ts. This file became home to a suite of pure functions designed to handle the intricate logic of workflow generation:
buildGroupWorkflowSteps(): This is the orchestrator. It takes a collection of selected action points and intelligently constructs a sequence of workflow steps. This isn't just concatenating individual workflows; it's about identifying common themes, potential consolidations (e.g., "Analyze these three related bug reports together"), and then generating N+2 steps – N steps for individual item processing, plus a coordination/group analysis step, and a final synthesis/review step.resolvePersonasForCategories(): Each action point might belong to a different category (e.g., "Bug Fix," "Feature Request," "Documentation"). This function ensures that the appropriate AI persona (e.g., "Senior Engineer," "Technical Writer") is assigned to the relevant steps within the group workflow, allowing for specialized processing.validateGroupBudget(): Generating AI content isn't free! We needed a mechanism to ensure that the combined complexity of a group workflow doesn't exceed predefined budget limits.uniqueLabel(): A small but crucial utility for ensuring that dynamically generated step labels remain unique, preventing UI clashes or confusion.
The decision to use pure functions here was deliberate. It made testing significantly easier. We now have 25 dedicated unit tests (tests/unit/group-prompt-builder.test.ts) covering everything from budget validation and label deduplication to the complex structure of step generation, persona assignment, and template variable references. This robust test suite gives us a lot of confidence in the core logic.
Bridging Frontend and Backend: The createGroupWorkflow Mutation
On the API side, we extended our existing src/server/trpc/routers/action-points.ts with a new createGroupWorkflow mutation. This tRPC endpoint acts as the gateway from the UI to our backend logic. It handles several critical tasks:
- Validation: Ensures all selected action points belong to the same project and none of them are already marked as "done." It also enforces the budget limits calculated by
validateGroupBudget(). - Auto-Discovery: Before creating the workflow, it automatically discovers relevant consolidations, insights, repositories, and personas associated with the selected action points. This context is vital for the AI to generate a truly useful workflow.
- Workflow Creation: It then calls into
buildGroupWorkflowSteps()and creates theWorkflowentity. A neat detail here is how we handle per-step persona overrides:persona: { connect: { id } }. This allows us to specify a particular persona for a specific step, overriding any default persona, which is essential forresolvePersonasForCategories()to work effectively. axiomMode: "all": For group workflows, we explicitly setaxiomModeto"all". This ensures the AI considers all available context from the selected items when generating prompts, rather than focusing on a single item.
Bringing it to Life: The User Interface
Of course, a powerful backend is useless without a great frontend experience. We updated two key areas:
src/app/(dashboard)/dashboard/action-points/page.tsx: Each action point card now sports a checkbox, allowing users to select multiple items across their entire dashboard. A floating action bar gracefully appears when items are selected, presenting the "Create Group Workflow" button. We also added cross-project validation here to guide users.src/app/(dashboard)/dashboard/projects/[id]/page.tsx: Within a specific project's detail page, theActionPointsTabnow supports multi-select. We even added a convenient "Select All Open" button, along with the same floating action bar, making it super easy to kick off a group workflow for all active tasks in a project.
Lessons Learned: The Set Spread Syntax Gotcha
Every development session has its moments of head-scratching, and this one was no exception. I ran into a peculiar TypeScript error: TS2802 — Set<string> can only be iterated with --downlevelIteration flag.
I was instinctively using the spread syntax ([...selectedIds]) to convert a Set into an Array, or to create a new Set from an existing one, like [...new Set(...)]. This is common practice in modern JavaScript. However, our project's tsconfig.json (likely due to a specific target version or other build configurations) doesn't have the --downlevelIteration flag enabled. This means that direct iteration over Sets using spread syntax isn't supported in the transpiled output.
The Fix (and a Project Gotcha):
The workaround, which is now explicitly documented in our internal CLAUDE.md (our project's "Known Issues" markdown), is to consistently use Array.from() instead of spread syntax when working with Sets and Maps.
// Failed: TS2802
// const selectedIdsArray = [...selectedIdsSet];
// const uniqueItems = [...new Set(someArray)];
// Correct way in this codebase:
const selectedIdsArray = Array.from(selectedIdsSet);
const uniqueItems = Array.from(new Set(someArray));
This was a good reminder that even seemingly standard JavaScript features can have environment-specific quirks, and internal documentation is invaluable for sharing these "gotchas" with the team.
What's Next? Solidifying with E2E Tests
The feature is functionally complete, but the job isn't truly done until we have robust end-to-end (E2E) tests. My immediate next steps involve:
- E2E Test 1: Verify the full flow: select 3+ action points from the dashboard, click "Create Group Workflow," and confirm the redirect to the new workflow's detail page.
- E2E Test 2: Run the created group workflow. This will involve verifying that the "Group Analysis" step correctly references all items, per-item steps utilize category-matched personas, and the final "Synthesis" step correctly pauses for user review.
- E2E Test 3: Test the project detail page flow: navigate to
/dashboard/projects/[id], use the "Select All Open" button, and confirm successful group workflow creation.
Finally, a minor UX enhancement I'm considering is adding a "Group Workflow" indicator badge on action point cards. Currently, they show the same "View" button as single-item workflows, which doesn't immediately convey that they're part of a larger, coordinated effort.
Overall, this was a challenging but rewarding session. Building features that empower users to manage complexity more efficiently is what makes development exciting. On to the E2E tests!
{"thingsDone":["Implemented group workflow creation for multiple action points, including multi-select UI, coordinated N+2 step workflow generation, cross-item dependency analysis, per-item persona assignment, and a synthesis review step.","Created `group-prompt-builder.ts` with pure functions for workflow steps, persona resolution, budget validation, and label deduplication.","Developed 25 unit tests for the `group-prompt-builder`.","Added `createGroupWorkflow` mutation to tRPC, handling validation, auto-discovery, and workflow creation with per-step persona overrides.","Updated dashboard and project detail pages with multi-select UI, floating action bars, and 'Select All Open' functionality."],"pains":["Encountered TS2802 error when using spread syntax on Sets due to missing `--downlevelIteration` flag.","Learned to consistently use `Array.from()` for Set/Map iteration in the codebase."],"successes":["Feature complete and deployed to main.","All type checks clean and 180/180 tests passing.","Successfully designed and implemented a complex backend logic for coordinating multiple items into a single, intelligent workflow.","Enhanced user experience with intuitive multi-select and group workflow creation UI."],"techStack":["TypeScript","tRPC","React","Next.js","Node.js","Jest (for unit tests)"]}