nyxcore-systems
5 min read

The Case of the Missing Fan-Out: Debugging Our LLM Pipeline's Configuration Flow

We built a killer LLM pipeline with fan-out capabilities, but a crucial feature wasn't fanning out. Tracing a missing config property taught us valuable lessons about data flow.

LLMAIPipelineDebuggingFrontendConfigurationNext.jsTypeScript

Every developer knows that feeling: you build a feature, you configure it, and it just… doesn't work. That's exactly where I found myself recently with a core component of our LLM-powered pipeline: the fan-out mechanism for our "Implementation Prompts" step.

Our goal was ambitious: generate not just one, but multiple, distinct, and downloadable sub-prompts based on different features or aspects of a larger task. Think of it like this: you give the AI a high-level goal, and it breaks it down into 8 independent, detailed implementation prompts, each accessible via its own tab in the UI. This "fan-out" capability is crucial for tackling complex problems in a structured, parallel fashion.

The Mystery of the Monolithic Output

We had the UI ready, the backend mutation capable of handling multiple outputs, and the StepConfig interface even had a fanOutConfig property. Everything looked correct on paper.

Yet, when I ran a test workflow using our "Deep Build Pipeline" template, the "Implementation Prompts" step stubbornly produced a single, giant, undifferentiated blob of text. No tabs, no individual downloads, no fan-out. Just one big, unparsed output. subOutputs was NULL when it should have been an array of distinct prompt objects.

My initial thought: "Did the LLM just ignore the instruction?" But deep down, I suspected a more mundane, yet equally frustrating, culprit: a missing link in the configuration chain.

Tracing the Data's Journey: The Root Cause Uncovered

Debugging a configuration issue often means tracing a variable's journey from its origin (a template, a database, a user input) to its final destination (a server-side processing function). Here's the path I mentally walked:

  1. The Template: Our StepTemplate in the database did have fanOutConfig correctly set for the "Implementation Prompts" step. So, the source was good.
  2. Client-side State: When a new workflow is created, we map the StepTemplate to a LocalStep object in our frontend state. I checked the stepTemplateToLocalStep() function. Aha! The fanOutConfig property was conspicuously absent from the copy operation. It was like buying a ticket for a train, but forgetting to actually get on.
  3. The Server Payload: Even if the local state was correct, was it being sent to the server? Our handleSubmit() function, responsible for sending the workflow configuration to the backend, also needed to include fanOutConfig in its payload. Another missing link.

The server-side stepConfigSchema and create mutation were already prepared for fanOutConfig. The problem was purely client-side – the data wasn't making it to the server.

The Three-Line Fix (and the "D'oh!" Moment)

Once the root cause was clear, the fix was almost embarrassingly simple. All changes were concentrated in new/page.tsx, our workflow creation page:

  1. Interface Augmentation: Ensure our client-side StepConfig interface explicitly included fanOutConfig. (This was already largely done, but good to double check).

    typescript
    // In new/page.tsx (conceptual)
    interface StepConfig {
        // ... other properties
        fanOutConfig?: FanOutConfig; // The crucial addition or verification
    }
    
  2. Template-to-Local Mapper: Copy fanOutConfig when converting a StepTemplate into a LocalStep.

    typescript
    // In stepTemplateToLocalStep() (conceptual)
    function stepTemplateToLocalStep(template: StepTemplate): LocalStep {
        return {
            // ... copy other properties
            fanOutConfig: template.fanOutConfig, // THIS was the missing line!
            // ...
        };
    }
    
  3. Submit Payload Passthrough: Include fanOutConfig when handleSubmit() sends the workflow configuration to the server.

    typescript
    // In handleSubmit() steps mapping (conceptual)
    const stepsPayload = localSteps.map(step => ({
        // ... other step properties
        config: {
            // ... other config properties
            fanOutConfig: step.config.fanOutConfig, // And this one!
        }
    }));
    

A few lines of code, a quick compile, and a deep breath. It's always the small things that hide in plain sight.

The Proof is in the Prompts: Verification and Triumph

To verify the fix, I needed a real-world test. Our existing completed workflows wouldn't magically fix themselves, as their configuration was already baked in. So, I picked a recent workflow (02534270), manually updated its fanOutConfig via SQL, and re-ran it.

The result? Pure satisfaction.

The "Implementation Prompts" step now correctly generated 8 distinct subOutputs entries. The UI rendered beautifully, displaying each of the 8 sub-prompts in its own tab, complete with individual copy and download buttons.

The workflow completed its 8 steps efficiently: ~45,000 tokens processed, costing around $0.50, all in about 9 minutes. The fan-out was finally, truly, fanning out!

Lessons Learned

This little adventure reinforced some critical debugging and development principles:

  1. Trace Your Data's Lifecycle: When a configured feature isn't working, always trace the configuration data from its source to its ultimate consumption point. Any transformation, mapping, or payload construction is a potential point of failure.
  2. Interfaces are Contracts, Mappers are Enforcers: While TypeScript interfaces provide compile-time safety, they don't guarantee runtime data population. Mappers (stepTemplateToLocalStep in this case) are where the contract is actually fulfilled. Double-check them!
  3. The Server Might Be Innocent: Don't always assume the backend is the problem. Sometimes, the server is perfectly capable, but the client isn't sending it the right instructions.
  4. Backfilling for Quick Verification: For critical fixes, manually backfilling or re-running a specific instance can be the quickest way to confirm success before rolling out a broader change.

What's Next on the Horizon?

With the fan-out pipeline robustly working, we're now free to focus on the next set of exciting challenges:

  • Enhancing our MemoryPicker with hybrid search (70% vector + 30% text) for even more relevant context retrieval.
  • Automating embedding generation for insights.
  • Implementing the plan from pure-dancing-valiant.md – which involves a comprehensive UI overhaul for reviewing key points and managing workflow lists.

It's these kinds of "small" but critical fixes that lay the groundwork for truly powerful AI applications. Here's to robust configuration and pipelines that actually do what they're told!


json
{"thingsDone":["Fixed fan-out configuration plumbing in `new/page.tsx`","Added `fanOutConfig` to `StepConfig` interface","Updated `stepTemplateToLocalStep()` to copy `fanOutConfig`","Modified `handleSubmit()` to pass `fanOutConfig` to server","Backfilled workflow `02534270` via SQL to verify fix","Successfully observed 8 sub-outputs in tabbed UI for the fan-out step"],"pains":["Implementation Prompts step ran as plain LLM (single output, no tabs)","`subOutputs` was NULL despite fan-out being configured","Root cause was `fanOutConfig` not being copied/passed correctly client-side"],"successes":["Identified and fixed the root cause quickly","Successfully verified the fix with a backfilled workflow","Enabled the core fan-out capability for LLM pipelines","Achieved expected tabbed UI with individual prompt download/copy"],"techStack":["Next.js","TypeScript","LLM Pipeline (custom)","Postgres (pgvector)","Client-side state management","UI/UX for multi-output steps"]}