Stripe, Shopify, and Salesforce all converged on the same pattern: LLM decides, code executes. Here's the architectural reasoning, the trade-offs, and when tool-calling actually makes sense.
Two ways to build an AI agent
Every AI agent system faces the same design question: who decides what happens next?
In a code-orchestrated system, the code assembles context, calls the LLM for a classification or generation, parses the structured output, and then executes a hardcoded branch. The LLM is a decision engine. The code is the orchestrator.
// Code-orchestrated: LLM classifies, code acts
const analysis = await llm.prompt({
system: contextForThisContact,
message: `Classify this reply: "${replyText}"`,
schema: { sentiment: 'positive | negative | question | ooo | referral' }
});
switch (analysis.sentiment) {
case 'positive':
await workspace.addTask(email, { title: 'Schedule demo', owner: 'sales-rep' });
await workspace.addUpdate(email, { summary: 'Lead expressed interest' });
break;
case 'negative':
await workspace.raiseIssue(email, { title: 'Opt-out requested', severity: 'critical' });
await workspace.markSequenceStatus(email, 'Opted Out');
break;
// ... each branch is explicit
}In a tool-calling system, the LLM receives a set of tools and decides which to call, when, and with what parameters. The LLM is both the decision engine and the orchestrator.
// Tool-calling: LLM decides AND acts
const result = await llm.promptWithTools({
system: `You are a reply analysis agent. Handle this reply appropriately.`,
message: replyText,
tools: [addTask, addUpdate, raiseIssue, markSequenceStatus, sendEmail, addNote]
});
// The LLM chose which tools to call and in what orderBoth work. The question is which failure modes you can tolerate.
What code-orchestrated systems actually look like in production
I built a prospecting agent system using the code-orchestrated pattern. It handles outbound email sequences, reply analysis, call outcome processing, account strategy, and task execution across two workspace collections (Contact and Company) with 20+ typed properties each.
Here's what the architecture looks like at the pipeline level:
- Cron triggers a pipeline (e.g., reply detected, sequence step due, call completed)
- Code assembles context: reads the contact digest, account strategy, sequence state, governance guidelines. This is 3-10 API calls depending on the pipeline, all done in code before the LLM sees anything.
- Single LLM call with structured output schema. The LLM receives all context and returns a typed JSON classification.
- Code parses the output and executes the appropriate branch: add tasks, raise issues, update sequence status, rewrite context, send notifications.
Thirteen pipeline files. Each one follows this exact pattern. The LLM never writes directly to the workspace. It classifies and generates. Code enforces.
The strengths are real:
Every workspace write is behind an explicit if/switch. You can read the code and know exactly what happens when a reply is negative. There's no chance the LLM decides to send another email instead of raising an issue.
One LLM call per pipeline. Token spend is predictable. No back-and-forth tool loops. No runaway costs from an agent that decided it needed "more context."
Debugging is straightforward. When something goes wrong, check the JSON the LLM returned and the branch the code took. Two things to inspect.
The honest cost of this approach
I used this system as the basis for evaluating whether tool-calling would be a better fit. I had to be honest about what the code-orchestrated pattern costs, because if you're invested in an architecture, you tend to overvalue its strengths and undercount its weaknesses.
The branching is growing. Reply analysis handles 6 sentiments. Call analysis handles 6 outcomes. Every new channel (WhatsApp, SMS, Slack DMs) needs another pipeline file with another set of branches. At 13 pipeline files, the system is manageable. At 30, it becomes the bottleneck.
Context assembly is a hidden bug surface. Each pipeline manually decides what context to include. generate-outreach assembles context differently than analyze-reply which assembles it differently than execute-task. If account strategy context is critical for a decision but one pipeline forgets to include it, the LLM makes a worse decision. And you'd never know, because the output still looks reasonable.
The "one LLM call" advantage is smaller than it appears. Yes, one call per pipeline. But the pre-assembly already makes 3-10 API calls per pipeline. A tool-calling agent would make similar calls, just in a different order. The marginal cost of tool calls vs. pre-assembled context is smaller than the headline comparison suggests.
JSON schema parsing fails silently. When the LLM returns malformed JSON, the parser falls back to defaults. A reply from someone saying "I want to buy right now" could get classified as neutral if the JSON parse fails. With tool calling, malformed calls are rejected, not silently defaulted.What Stripe, Shopify, and Salesforce learned
Three companies with production agent systems at scale have published detailed accounts of their architectures. The convergence is striking.
Stripe: 1,300+ autonomous PRs per week
Stripe's Minions architecture uses what they call the Hybrid Blueprint pattern. Each agent is narrowly scoped, single-task, single LLM call. The LLM writes code. A deterministic system runs the linter. The LLM fixes errors. The deterministic system commits.
The agents don't have broad tool access. They have one job, one context, one output. Stripe's engineering team is explicit: "Context engineering does the heavy lifting: the quality of context assembled before the LLM call determines output quality."
This is code-orchestrated at its purest. The LLM is powerful but constrained. The system is reliable because the blast radius of any single LLM failure is limited to one narrowly scoped task.
Shopify: the 50-tool wall
Shopify's Sidekick started with tool-calling and hit what they call "Death by a Thousand Instructions" at approximately 50 tools. The system prompt became an unwieldy collection of special cases. Tool selection accuracy degraded. Edge cases multiplied.
Their solution was Just-in-Time Instructions: relevant guidance returned alongside tool data exactly when needed, not crammed into the system prompt. They also built specialized LLM judges for evaluation, improving their Cohen's Kappa from 0.02 (barely better than random) to 0.61 (near human baseline of 0.69).
Their recommendation for teams starting out: "Stay simple. Resist adding tools without clear boundaries. Simple single-agent systems can handle more complexity than expected."
Salesforce: when LLM instructions aren't enough
Salesforce documented a customer case that crystallizes the core risk. A deployment with 2.5 million users found that satisfaction surveys were randomly not being sent despite clear LLM instructions to send them. The instructions were correct. The model understood them. But understanding instructions and reliably executing them every single time are different things.
The fix: deterministic triggers. Code checks the condition. Code sends the survey. The LLM has no role in the execution path.
Salesforce now recommends "hybrid reasoning": LLM flexibility for tasks that benefit from judgment, deterministic logic for tasks that require consistency.
The hybrid pattern
The emerging consensus from production isn't "code-orchestrated vs. tool-calling." It's "code-orchestrated for enforcement, tool-calling for flexibility, guardrails for both."Here's what that architecture looks like:
┌─────────────────────────────────────────────────────────┐
│ GUARDRAIL LAYER (code, always runs) │
│ - Opt-out check before ANY outreach executes │
│ - Rate limiting on send operations │
│ - Sequence status validation before email │
│ - Parameter validation on every tool call │
│ - Compliance policy enforcement │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ DECISION LAYER (LLM, one of two modes) │
│ │
│ Mode A: Classification (code-orchestrated) │
│ - Reply sentiment analysis │
│ - Call outcome classification │
│ - Sequence step decisions │
│ → LLM returns typed JSON, code acts │
│ │
│ Mode B: Tool-calling (with guardrails) │
│ - Generic task execution │
│ - Human-created custom tasks │
│ - Novel situations not covered by branches │
│ → LLM selects tools, guardrails validate each call │
└─────────────────────────────────────────────────────────┘
The key insight: the guardrail layer runs regardless of which mode the LLM operates in. If an opt-out must be enforced, code enforces it. If a rate limit must be respected, code respects it. The LLM can't bypass the guardrail layer by choosing different tools or returning different JSON.
When to use which pattern
After working through this architecture and studying the production data, here's how I think about the decision.
Use code-orchestrated (Mode A) when:
The failure mode is unacceptable. Opt-out enforcement, compliance checks, data deletion, billing operations. If the LLM forgetting to call a tool means legal exposure or customer harm, code should own the entire flow.
The branches are known. Reply sentiments, call outcomes, sequence logic. If you can enumerate every case, code handles it more reliably than tool selection. A switch statement has a 0% tool-selection error rate.
Debugging speed matters. When a customer asks "why did the system do X?", you need to answer in minutes. With code-orchestrated, you check one JSON output and one code branch. With tool-calling, you trace a multi-step conversation.
Cost predictability matters. One LLM call per pipeline, every time. No variance from tool-calling loops, retries, or the model deciding it needs "more context."
Use tool-calling (Mode B) when:
The task is genuinely open-ended. A human creates a task: "Research this company's expansion into APAC and prepare talking points." No hardcoded branch can handle arbitrary human intent. The LLM needs tools to pull context and compose actions.
Novel combinations are valuable. A reply mentions a competitor, expresses interest, AND asks a technical question. Code-orchestrated handles this as one of the enumerated sentiments. Tool-calling can compose a custom response: add a competitive intel note, create a task for the sales engineer, and draft a reply that addresses the technical question.
The cost of rigidity exceeds the cost of unpredictability. When you're adding a new else if branch every week because edge cases keep appearing, the branching approach has become the problem. Tool-calling absorbs edge cases by design.
Always use guardrails when:
There is no "when." Guardrails run on every path, every time. They are the non-negotiable layer.
The migration path
If you're running a code-orchestrated system today and considering tool-calling, here's the sequence that minimizes risk.
Phase 1: Add validation to what you have. Log and alert on JSON parse failures instead of silently defaulting. Add monitoring dashboards for classification distribution. Catch drift before it becomes a problem.
Phase 2: Add tool-calling to the most constrained pipeline. In most systems, this is the generic task executor: the one place where the LLM already feels limited by hardcoded branches. Give it tools with guardrails. Measure error rates against your code-orchestrated pipelines.
Phase 3: Migrate reply and call analysis if the data supports it. If tool-calling error rates are acceptable (and your guardrails catch the failures that matter), convert the 6-branch sentiment handler into a tool-calling agent. Keep the guardrail layer.
Phase 4: Tool search if you exceed 30 tools. Anthropic's data shows accuracy drops significantly past this threshold. Implement a tool filtering layer that loads only the 3-5 relevant tools per request.
The key: each phase is independently valuable and independently reversible. If Phase 2 reveals unacceptable error rates, you revert to code-orchestrated for that pipeline and lose nothing. You're not committing to a full migration. You're running a controlled experiment.
What I chose and why
For the prospecting agent going to production, I'm keeping code-orchestrated for all safety-critical flows: reply handling, sequence engine, opt-out enforcement, account strategy. These are the flows where a single missed tool call means real damage, and the branches are known.
I'm adding tool-calling with guardrails for the generic task executor, where human-created tasks need flexibility the current branches can't provide.
And I'm building the guardrail layer as a separate concern, enforcing invariants regardless of which mode the LLM operates in.
The models will improve. The benchmarks will improve. But the principle holds: let the LLM reason where reasoning matters, and let code enforce where enforcement matters. Don't ask one system to do both.
References: Berkeley Function Calling Leaderboard, ToolScan (ICLR 2025), Anthropic Advanced Tool Use, Stripe Minions Architecture, Shopify Engineering: Production Agentic Systems, Salesforce Hybrid Reasoning, FutureAGI: Tool Chaining Failures, OpenAI Structured Outputs, Chroma: Context Rot, ZenML: 1,200 Production Deployments