How to Convert Natural Language to Structured Tool Calls in LLM Agents

To convert natural language to structured tool calls, prompt the LLM to emit JSON describing the desired function and parameters, parse the output into a typed object, and dispatch to the appropriate tool implementation based on the function name.

The humanlayer/12-factor-agents repository demonstrates this pattern in Factor 1 — Natural Language to Tool Calls, providing a deterministic bridge between ambiguous human input and executable programmatic actions. This approach keeps your agent's business logic type-safe and testable while leveraging the LLM's reasoning capabilities to interpret intent.

The Three-Step Pattern (Factor 1)

The architecture defined in content/factor-01-natural-language-to-tool-calls.md breaks the conversion process into three distinct phases: generation, parsing, and dispatch.

Step 1: Prompting for Structured JSON

Instead of allowing the LLM to generate free-form text, constrain the output to a predictable JSON schema. The model receives the raw user request (e.g., "create a payment link for $750 to Terri…") and returns a structured payload:

{
  "function": {
    "name": "create_payment_link",
    "parameters": {
      "amount": 750,
      "customer": "cust_128934ddasf9",
      "product": "prod_8675309",
      "price": "prc_09874329fds",
      "quantity": 1,
      "memo": "…"
    }
  }
}

This example appears in the repository's documentation (lines 13–29), showing exactly how the LLM should format its response to enable deterministic downstream processing.

Step 2: Parsing into Typed Objects

Once the LLM returns the JSON description, parse it into a structured object that your agent code can inspect. In the reference implementation found at lines 35–44 of the Factor 1 documentation, the determineNextStep method returns an object with accessible function and parameters fields:

next_step = await llm.determineNextStep(user_prompt)

# next_step.function -> "create_payment_link"

# next_step.parameters -> {amount: 750, ...}

Strong typing here ensures that your IDE and runtime can validate the shape of incoming tool calls before execution.

Step 3: Dispatching to Tool Implementations

With the structured call in hand, route to the concrete implementation using the function name. The repository shows a simple dispatch pattern at lines 45–50:

if next_step.function == "create_payment_link":
    stripe.paymentlinks.create(next_step.parameters)
elif next_step.function == "send_email":
    email_service.send(next_step.parameters)

For unrecognized functions, implement a fallback branch that logs the error, requests clarification, or triggers a default handler—preventing the agent from crashing on hallucinated tool names.

Why This Architecture Works

Determinism — Constraining the LLM to a fixed JSON schema ensures downstream code remains type-safe and unit-testable. You validate inputs against schemas before execution, eliminating ambiguity in the "what" before handling the "how".

Separation of Concerns — The LLM decides what action to take; your agent's runtime handles how to execute it. This aligns with 12-Factor principles by keeping business logic distinct from orchestration and allowing each component to evolve independently.

Extensibility — Adding new capabilities requires only defining a new function name in the prompt template and adding a corresponding branch to the dispatch block. No model retraining or fine-tuning is necessary to support additional tools.

Implementation Examples

Below are production-ready implementations following the exact patterns from the 12-factor-agents source code.

Python Implementation

async def handle_user_request(user_prompt: str):
    # 1️⃣ Convert natural language to structured call

    next_step = await llm.determineNextStep(user_prompt)
    
    # 2️⃣ Dispatch based on function name

    if next_step.function == "create_payment_link":
        stripe.paymentlinks.create(next_step.parameters)
    elif next_step.function == "send_email":
        email_service.send(next_step.parameters)
    else:
        # Unknown function handling

        logger.warning(f"Unsupported tool: {next_step.function}")
        return "I don't know how to perform that action."

This mirrors the implementation shown in content/factor-01-natural-language-to-tool-calls.md (lines 35–50).

TypeScript Implementation

type ToolCall = {
  function: {
    name: string;
    parameters: Record<string, unknown>;
  };
};

async function runTool(call: ToolCall) {
  switch (call.function.name) {
    case "create_payment_link":
      await stripe.paymentLinks.create(call.function.parameters);
      break;
    case "add_calendar_event":
      await calendar.addEvent(call.function.parameters);
      break;
    default:
      console.warn(`Unknown tool: ${call.function.name}`);
  }
}

// Usage
const structured: ToolCall = await llm.determineNextStep(userInput);
await runTool(structured);

The TypeScript version adds explicit type safety while maintaining identical logic to the Python reference implementation.

Integration with the 12-Factor Agent Framework

Factor 1 feeds directly into Factor 3 — Own Your Context Window (content/factor-03-own-your-context-window.md), where the structured output is stored in the agent's execution state. This enables feedback loops where the results of tool calls can be re-injected into subsequent prompts.

The repository manages these patterns as composable modules. You can combine natural-language-to-tool-calls with other factors like pause/resume, error handling, or human-in-the-loop approvals without architectural refactoring. The starter template found in packages/create-12-factor-agent/template/README.md provides scaffolded code that implements this integration out of the box.

Summary

  • Constrain LLM outputs to JSON schemas describing function names and parameters to convert natural language into structured tool calls.
  • Parse and validate the generated JSON into typed objects before dispatch to ensure runtime safety.
  • Use explicit dispatch logic (if/elif chains or switch statements) to route calls to concrete implementations, with fallbacks for unknown functions.
  • Reference the source at content/factor-01-natural-language-to-tool-calls.md for the canonical implementation and line-specific examples.

Frequently Asked Questions

What is the 12-Factor Agents approach to tool calling?

The 12-Factor Agents approach treats natural language to structured tool calls as the first architectural layer (Factor 1). It requires prompting the LLM to emit JSON descriptions of desired actions, then parsing and dispatching those descriptions through type-safe code. This pattern appears in content/factor-01-natural-language-to-tool-calls.md and emphasizes deterministic execution over raw LLM output.

How does the dispatch pattern handle unknown functions?

The dispatch pattern includes a mandatory fallback branch that catches unrecognized function names. As shown in lines 45–50 of the Factor 1 documentation, when the LLM returns a function name not present in the dispatch table, the agent logs the error and either requests clarification or returns a default "I don't know how to perform that action" message. This prevents execution of undefined or hallucinated tools.

Why use JSON instead of native function calling APIs?

JSON output provides explicit visibility and control over the intermediate representation between the LLM and your business logic. While proprietary function-calling APIs abstract this layer, the 12-Factor approach in packages/walkthroughgen/prompt.md demonstrates that raw JSON allows you to validate schemas, audit decision chains, and maintain portability across different LLM providers without vendor lock-in.

How does Factor 1 interact with other factors in the framework?

Factor 1 produces structured data that Factor 3 (Own Your Context Window) stores in the agent's execution state. This structured representation enables the context management, human oversight, and error recovery patterns described in subsequent factors. The modular architecture allows you to adopt Factor 1 independently while maintaining compatibility with the full 12-factor ecosystem.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →