Compiler to LLM Workflows
How intent-first development compressed our iteration loops—and why the quality surprised us more than the speed.
Thesis: Switching to an LLM-based workflow doesn't replace engineering discipline—it compresses iteration loops. From "compile cycle" to "intent → generation → optimization → validation." And when you add config + rule-driven constraints, the quality jump is wild.
The Old World
You know the loop. You've lived it for years.
Edit code. Run the compiler. See red. Fix the error. Run again. See different red. Fix that. Run again. Finally green—but now a test fails. Fix the test. Run the compiler again.
This is the compiler-driven workflow. It's reliable. It's rigorous. And it's slow in ways we stopped noticing.
The hidden costs compound:
- Context switching: jump from code to error output to docs to code
- Brittle refactors: rename a function, break 47 files
- Small change, big cascade: one type change propagates through six layers
- Scaffolding tax: writing boilerplate before you can write logic
We accepted this. It was just how software got built.
The Compiler Workflow: Why It's Great (and Where It Hurts)
Let me be fair. Compilers are incredible.
TypeScript's type checker catches entire categories of bugs before runtime. Rust's borrow checker prevents memory errors at compile time. Go's simplicity makes builds fast and predictable.
Compilers enforce correctness. That's their job, and they do it well.
But compilers only tell you what's wrong after you wrote it.
They don't help with:
- Scaffolding new features
- Architecture decisions
- Migration strategies
- Refactor planning
- Documentation-driven development
- Generating test cases
- Exploring API designs
The compiler is a gate. It's not a collaborator.
The LLM Workflow: Intent-First Engineering
Here's the shift. Instead of starting with code, start with intent.
flowchart LR
A[Describe Intent]:::primary --> B[Generate Code]:::secondary
B --> C[Review Output]:::secondary
C --> D[Optimize w/ Rules]:::secondary
D --> E[Validate tests]:::secondary
E --> F{"Ship or Repeat?"}:::accent
F -->|Repeat| A
You write a spec. The LLM generates an implementation. You review, request changes, and the LLM optimizes based on your constraints. Then you validate with tests, types, and linting.
The compiler still runs. But it runs at the end of the loop, not the beginning.
The "Config + Text" Unlock: Controlling the Output
Here's what took me too long to understand: the real power isn't generation—it's guided optimization.
Raw code generation is impressive but inconsistent. What makes it reliable is giving the LLM constraints:
- A ruleset or conventions document
- A config file (tsconfig, eslint, prettier)
- Clear requirements in text (performance goals, API patterns, error handling)
- Examples of existing code in the repo
When you provide these, the LLM stops guessing and starts conforming.
| Input Type | What It Controls |
|---|---|
| Repo conventions | Naming, file layout, module boundaries |
| Lint config | Style consistency, import ordering |
| Strict TypeScript | Null safety, type narrowing |
| API contract examples | Request/response shapes, error formats |
| Test requirements | Coverage expectations, edge cases |
| Performance constraints | Bundle size, latency targets, memory limits |
The pattern that works:
"Generate a service that follows our API conventions in /docs/api-contracts.md,
uses the error handling pattern from /src/lib/errors.ts,
and includes tests covering the edge cases listed below."
LLMs are best when treated like a code generator + optimizer under constraints.
A Concrete Before/After: Persona Creation Flow
We had a workflow generation system. The original approach: compile a workflow spec into JSON, deploy it to an API.
Before (compiler-first):
// Manual: parse intent, build spec, compile to JSON, handle errors
const intent = parseNaturalLanguage(userInput);
const spec = intentToSpec(intent);
const { workflow_def, proto_config } = compileWorkflow(spec);
// Deploy - fails with "Internal Server Error"
await client.updateAiEmployee({
persona_id: newId,
workflow: workflow_def,
proto_config: proto_config,
});
// Debug for 3 hours. The compiled format doesn't match the API.
The compiled output looked correct. TypeScript was happy. But the API rejected it because the structure didn't match what the platform expected.
After (LLM-assisted with constraints):
// Intent: "Create persona from template, configure settings, preserve template workflow"
// Constraints: Don't replace template workflow. Merge proto_config.
// Reference: API format in /docs/api-contracts.md
// Step 1: Create from template (valid structure guaranteed)
const result = await client.createAiEmployee({ name, template_id });
// Step 2: Fetch to get template's structure
const persona = await client.getPersonaById(result.persona_id);
// Step 3: Merge generated config with template config
const mergedConfig = mergeProtoConfig(persona.proto_config, generatedConfig);
// Step 4: Update config only — don't touch workflow
await client.updateAiEmployee({
persona_id: result.persona_id,
proto_config: mergedConfig,
});
The LLM didn't just generate code. It understood the constraint—"preserve the template's workflow structure"—and produced an architecture that worked on the first deploy.
The Moment It Got Astounding
It wasn't just faster. The output got better every pass—and that part surprised me.
After a few optimization cycles with clear constraints:
- Fewer mistakes: the LLM caught edge cases I would have missed
- Better structure: separation of concerns I wouldn't have thought to add
- Consistent patterns: naming, error handling, logging—all uniform
- Cleaner diffs: changes were surgical, not sprawling
- Complete edge-case handling: null checks, error boundaries, fallbacks
I expected speed. I didn't expect quality.
The second pass felt like working with a senior engineer who never gets tired.
Why This Works: Compilers Validate, LLMs Accelerate Intent
The mental model that helped me:
| Role | Tool | What It Does |
|---|---|---|
| Correctness gate | Compiler | Rejects invalid code |
| Iteration accelerator | LLM | Generates and refines code from intent |
| Guardrails | Rules/config | Turns acceleration into reliability |
The compiler is still essential. But it moved from "primary feedback loop" to "final validation."
The LLM handles the exploration. The compiler handles the proof.
What Didn't Work (Honest Pitfalls)
This isn't magic. Here's what went wrong:
- Hallucinated APIs: The LLM confidently called methods that don't exist
- Over-engineering: Asked for a simple function, got an abstract factory
- "Confident wrong": Syntactically perfect code with completely wrong logic
- Runtime failures: Code compiles but crashes on real data
- Context drift: Long sessions where the LLM forgets earlier constraints
Trust, but verify.
Every generated block needs:
- Type checking
- Tests
- Human review
The LLM is a collaborator, not an oracle.
Practical Workflow You Can Copy
Here's the playbook:
The Loop
- Write a short spec — 3-5 sentences describing what you need
- Include constraints — performance, style, patterns to follow
- Reference configs — point to tsconfig, lint rules, existing examples
- Generate — let the LLM produce an initial implementation
- Run tests — validate immediately
- Request optimization — "make this follow our error handling pattern"
- Keep diffs small — one change per pass
- Lock patterns with rules — add conventions to your repo docs
Checklist
Before generating:
- [ ] Spec written (what, not how)
- [ ] Constraints listed (performance, style, patterns)
- [ ] Config files referenced (tsconfig, eslint, prettier)
- [ ] Examples linked (existing code to follow)
After generating:
- [ ] Types check
- [ ] Tests pass
- [ ] Linting clean
- [ ] Human reviewed
- [ ] Diff is readable
The New Loop Is Addictive
Compiler workflows are still essential. TypeScript isn't going anywhere. Neither is testing, or code review, or architectural discipline.
But the day-to-day loop changes dramatically.
Instead of:
flowchart LR
A[write]:::secondary --> B[compile]:::secondary --> C[error]:::warning --> D[fix]:::secondary --> E[compile]:::secondary --> F[error]:::warning --> G[fix]:::secondary --> H[compile]:::secondary --> I[done]:::accent
It becomes:
flowchart LR
A[describe]:::primary --> B[generate]:::secondary --> C[review]:::secondary --> D[optimize]:::secondary --> E[validate]:::secondary --> F[done]:::accent
The combination of generation + optimization under config/text constraints is the real upgrade. Not because it removes engineering judgment—but because it compresses the iteration cycle until the feedback loop feels instant.
You still need to know what good code looks like. But now you can get there in minutes instead of hours.
Try This Today
Pick a task you've been putting off:
- A migration you've been dreading
- A refactor that touches too many files
- A new service you haven't scaffolded yet
Write a 5-sentence spec. List 3 constraints. Reference your config files.
Generate. Review. Optimize. Validate.
See how far you get in 30 minutes.
I think you'll be surprised.
This post is part of a series on AI-assisted development workflows. See Cursor Rules vs MCP for separating policy from data, and The MCP Mental Model for the conceptual foundation of knowledge-first development.