← Back to blog

Integrating AI with real systems: tool calling, function schemas, deterministic side-effects, and idempotency

The easiest AI demo is a chat box that answers questions. The moment you connect that chat box to real systems-creating tickets, updating records, sending emails, triggering payments-you have moved from “helpful assistant” to “autonomous actor”.

That transition is where most teams get hurt.

Not because the model is malicious, but because models are probabilistic. They guess. They improvise. They sometimes produce outputs that look correct but are subtly wrong. If you let that directly trigger side-effects, you will eventually ship an incident.

This post covers the integration patterns that make AI usable in production: tool calling, function schemas, deterministic side-effects, and idempotency. The theme is simple: treat the model as an untrusted planner and keep execution strict.

Tool calling: separate “deciding” from “doing”

Tool calling (function calling) is a clean way to connect an LLM to your systems:

  • The model proposes an action (“call this tool with these arguments”).
  • Your application validates it.
  • Your application executes it.
  • The model gets the tool result and continues.

The value is control. You are not parsing natural language with regex and hope. You define explicit operations and constrain what the model can do.

A practical rule:

The model can suggest. The system enforces.

If the system cannot enforce, the tool should not exist.

Keep the tool surface small

A common mistake is exposing a rich internal API catalogue and expecting the model to “use it responsibly”. It won’t. It will use whatever seems plausible.

Start with a minimal set of tools that map to real user outcomes:

  • search customer
  • get account status
  • create support ticket
  • draft email (no send)
  • schedule meeting (requires confirmation)

Add tools only when you can explain:

  • the business value,
  • the risk,
  • and the guardrails.

Function schemas: if you don’t define structure, you don’t have safety

The point of function schemas is not convenience. It is validation.

Your tool schema should be strict enough that:

  • invalid requests fail fast,
  • missing fields are obvious,
  • and ambiguous requests force clarification.

Design principles for schemas

  1. Prefer narrow tools over wide tools
    • Bad: update_record(table, json_blob)
    • Better: update_customer_email(customer_id, new_email)

Wide tools invite unsafe improvisation and broaden blast radius.

  1. Use explicit types and enums
    • priority: "low" | "medium" | "high"
    • channel: "email" | "sms" | "push"

Free-form strings are where mistakes and prompt injection hide.

  1. Separate “lookup” from “mutate”

    • Read-only tools should be easy and plentiful.
    • Write tools should be few and heavily checked.
  2. Force references, not raw content

    • Prefer IDs over names.
    • Prefer document_id over “paste the document”.

This improves auditability and reduces data leakage.

  1. Make confirmation explicit For destructive or external actions, design the schema so the model cannot execute without a human-confirmed token:
    • confirm_token that only your UI can supply
    • or a two-step flow: prepare_action then commit_action

If the model can trigger irreversible actions in one step, it eventually will.

Validation is not optional

Treat tool calls like public API requests:

  • validate required fields,
  • validate formats,
  • validate authorisation,
  • validate business rules.

The model is not a trusted client.

Deterministic side-effects: make execution boring

“Deterministic side-effects” means that given the same validated request, the system performs the same action every time, and the outcome does not depend on model phrasing, model temperature, or interpretation.

This matters because the model should not be the place where business logic lives.

Where teams go wrong

They let the model encode business rules in prose:

  • “If the customer is premium, apply a discount”
  • “Only email during office hours”
  • “Don’t create a ticket if one already exists”

That is brittle. It is not testable. It will drift.

The safer pattern

  • The model gathers intent and proposes a tool call.
  • The service enforces policy and rules deterministically.

Examples:

  • Permission checks happen in code, not in prompt.
  • Office hours and holiday rules happen in code, not in prompt.
  • Deduplication happens in code, not in prompt.
  • Pricing and discount rules happen in code, not in prompt.

If you cannot unit test it, it does not belong in the model layer.

Prefer “plan then execute”

For multi-step tasks, have the model produce a plan that is non-binding, then execute each step through validated tools.

This reduces the chance that a single mistaken inference becomes a chain reaction.

Idempotency: assume retries, duplicates, and partial failures

If you connect AI to tools, assume:

  • network timeouts,
  • tool errors,
  • model retries,
  • user refreshes,
  • and duplicate submissions.

Without idempotency, retries become duplicate side-effects:

  • two tickets created,
  • two emails sent,
  • two refunds issued,
  • two calendar invites booked.

Idempotency is how you keep the system safe under real conditions.

Practical idempotency patterns

  1. Idempotency keys for mutating operations Every “write” tool should accept or be wrapped with an idempotency key:
    • generated by your application per user intent
    • stable across retries
    • stored with the resulting operation

If the same key is seen again, return the original result instead of repeating the action.

  1. Natural keys and deduplication When idempotency keys are not practical, use natural uniqueness constraints:
    • “one active ticket per customer per incident type per day”
    • “one refund per order line”
    • “one calendar booking per organiser + start time + attendees”

Make those constraints real in storage, not implied.

  1. Two-phase commit for high-risk actions
    • prepare_refund(...) returns a preview and a commit_token
    • only a user-confirmed flow can call commit_refund(commit_token)

This stops the model from executing irreversible actions as part of a speculative chain.

  1. Outbox pattern for external side-effects If tool calls trigger external actions (email/SMS/webhooks), use an outbox table:
    • write an event transactionally,
    • process it asynchronously,
    • ensure exactly-once delivery semantics as far as practical.

This makes retries safe and gives you observability.

The integration checklist that keeps you out of trouble

A practical baseline for agent/tool integrations:

  • Tools are explicit and minimal; read tools are wider than write tools.
  • All tools have strict schemas with types, enums, and clear required fields.
  • The system validates and authorises every tool call.
  • Business rules live in deterministic services, not in prompts.
  • High-risk actions require confirmation or a two-step commit.
  • Every mutating tool is idempotent (keys or dedup constraints).
  • Tool executions are audited with correlation IDs and input/output metadata.
  • Failures degrade safely (no partial multi-step side-effects without recovery paths).

Closing thought

When you integrate AI with real systems, you are adding a probabilistic interface to operations.

Tool calling with strict schemas keeps the model on the planning side. Deterministic execution keeps behaviour testable. Idempotency keeps retries from becoming incidents.

Red Marina Assistant