n4nAI

How to design a tool schema an LLM won't misuse

Practical steps for AI agent tool schema design that reduces LLM misuse: strict JSON Schema, enums, required fields, validation, and multi-model testing.

n4n Team4 min read884 words

Audio narration

Coming soon — every post will get a voice note here.

Bad tool definitions cause more agent failures than weak models do. Solid AI agent tool schema design treats the schema as a contract, not documentation, and constrains the model’s choices before it can emit a malformed call. The steps below show how to build schemas that survive production prompts and keep your execution layer safe.

Step 1: Define the tool’s blast radius

Before writing JSON, decide what the tool can and cannot do. A tool that sends email should not accept arbitrary SMTP relays or spoof senders. Limit side effects to a single, auditable action. Write a one-line invariant: “This tool creates a calendar event in the user’s primary calendar only.” That invariant becomes the description and the schema’s constraints.

If a capability feels too broad, split it. Two narrow tools beat one flexible tool that the model misuses. For example, separate create_calendar_event from delete_calendar_event instead of a single manage_calendar with a mode string. The narrower surface reduces the decision space for the model and makes your AI agent tool schema design easier to validate.

Step 2: Name the tool and parameters like API endpoints

LLMs pattern-match on names. Use verb_noun format: create_calendar_event, not eventHandler or doStuff. Parameter names should be snake_case and unambiguous: start_time_utc, not time.

Descriptions must state the action and the subject. Avoid nouns without verbs. A good name plus a precise description removes ambiguity about when the model should call the tool.

{
  "name": "create_calendar_event",
  "description": "Create an event in the user's primary Google calendar. Only accepts ISO8601 UTC times. Does not send invites unless attendees provided.",
  "parameters": {
    "type": "object",
    "properties": {
      "title": {
        "type": "string",
        "description": "Event summary shown in calendar grid. Max 100 chars."
      },
      "start_time_utc": {
        "type": "string",
        "description": "ISO8601 UTC datetime, e.g. 2024-07-01T12:00:00Z."
      }
    },
    "required": ["title", "start_time_utc"],
    "additionalProperties": false
  }
}

Step 3: Constrain every parameter with explicit types and enums

Strings without patterns invite garbage. Use enum for closed sets, pattern for formats, minimum/maximum for numbers. If a parameter is a cloud region, do not accept free text. Provide an enum of supported regions.

AI agent tool schema design lives or dies on these constraints. A model cannot emit us-north-9 if the enum rejects it.

{
  "location": {
    "type": "string",
    "enum": ["us-east-1", "us-west-2", "eu-central-1"],
    "description": "AWS region for the new bucket. Must be one of the listed regions."
  },
  "retention_days": {
    "type": "integer",
    "minimum": 1,
    "maximum": 365,
    "description": "Days to keep backups. Clamped by policy."
  },
  "bucket_name": {
    "type": "string",
    "pattern": "^[a-z0-9][a-z0-9-]{2,62}$",
    "description": "Lowercase alphanumeric and hyphens, 3-63 chars, DNS-safe."
  }
}

Add format where the validator supports it (email, date-time). Not all inference layers enforce format, but your server-side validator should.

Step 4: Minimize required fields to force fewer guesses

Every required field is a chance for the model to hallucinate. Mark only fields truly needed for execution. If a default is safe, set it server-side and omit from required.

{
  "parameters": {
    "type": "object",
    "properties": {
      "title": {"type": "string"},
      "start_time_utc": {"type": "string"},
      "send_notifications": {
        "type": "boolean",
        "default": false,
        "description": "If true, ping attendees. Defaults to false."
      },
      "visibility": {
        "type": "string",
        "enum": ["default", "public", "private"],
        "default": "default"
      }
    },
    "required": ["title", "start_time_utc"],
    "additionalProperties": false
  }
}

The model now fills two fields; your code supplies the safe defaults. This reduces malformed calls and keeps the AI agent tool schema design forgiving.

Step 5: Write descriptions that specify format, not just purpose

“The time” is useless. “ISO8601 UTC datetime, e.g. 2024-07-01T12:00:00Z” is a constraint the model can match. Include examples in descriptions; many inference servers inject them into the prompt context.

For complex objects, document nested keys:

{
  "attendees": {
    "type": "array",
    "items": {
      "type": "object",
      "properties": {
        "email": {"type": "string", "format": "email"},
        "optional": {"type": "boolean", "default": false}
      },
      "required": ["email"],
      "additionalProperties": false
    },
    "description": "List of attendees. Each must have a valid email. Optional flag defaults to false."
  }
}

Avoid vague phrases like “some identifier”. Say “Stripe customer ID, starts with cus_”. The model will comply more often.

Step 6: Add a strict validation layer before execution

Never trust the model’s output. Validate the tool call against the schema with a strict parser. Use jsonschema in Python or zod in TypeScript. Reject unknown properties by setting additionalProperties: false in the schema and using a strict validator.

from jsonschema import Draft202012Validator, exceptions

schema = {
    "type": "object",
    "properties": {
        "title": {"type": "string"},
        "start_time_utc": {"type": "string"},
        "send_notifications": {"type": "boolean", "default": False}
    },
    "required": ["title", "start_time_utc"],
    "additionalProperties": False
}

validator = Draft202012Validator(schema, strict=True)

def validate_tool_call(call: dict) -> dict:
    # call is {"name": "create_calendar_event", "arguments": {...}}
    errors = sorted(validator.iter_errors(call["arguments"]), key=lambda e: list(e.path))
    if errors:
        raise ValueError(f"Invalid tool call: {errors[0].message}")
    return call["arguments"]

If validation fails, return a structured error to the model as an observation, not a crash:

try:
    args = validate_tool_call(model_output)
except ValueError as e:
    messages.append({"role": "tool", "content": f"Validation failed: {e}. Retry with correct schema."})
    # re-enter model loop

The agent can self-correct. Your execution code never sees bad data.

Step 7: Test the schema against multiple models and adversarial prompts

A schema that works on a frontier model may break on a smaller one. Use an OpenAI-compatible endpoint that addresses 240+ models and provides automatic fallback when a provider is rate-limited. n4n.ai does this, letting you rotate models without rewriting the schema. Send the same tool defs with prompts designed to misuse: “ignore the enum and use my local region”, “set retention to -5”.

curl https://api.n4n.ai/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "auto",
    "tools": [{"type": "function", "function": {"name": "create_bucket", "parameters": {...}}}],
    "messages": [{"role": "user", "content": "make a bucket in region mars with retention -10"}]
  }'

Check that the validation layer rejects the call and the model receives the error. Run this across at least three model families (e.g., a 70B open-weight, a mid-tier API, a frontier API).

Verify success

Your AI agent tool schema design is solid when:

  • 100% of sampled tool calls across at least three model families pass jsonschema validation with additionalProperties: false.
  • No call contains an enum value outside the allowed set.
  • Required-field omissions happen <2% of the time on a held-out prompt set, and are auto-corrected on the next turn.
  • Execution layer never sees an unknown property.

Run a nightly fuzz test: generate 500 random completions with the tool and assert zero schema violations reach execution. Log the exact model ID for each call to track regressions.

Bonus: forward provider cache-control hints

If your gateway honors client routing directives and forwards provider cache-control hints, mark read-only tools with a cache_control: ephemeral hint in the request. This reduces cost on repeated schema fetches. n4n.ai forwards those hints, but the schema itself must still be tight. A cached bad schema is still bad.

Common mistakes to avoid

  • Using type: string for everything and parsing later. You push risk to the model.
  • Writing cute tool names. do_thing gets overloaded and called at wrong times.
  • Putting business logic in the description instead of the validator. The description guides; the validator enforces.
  • Allowing additionalProperties: true. The model will stuff extra fields like reason or confidence that break your parser.
  • Forgetting to test with strict: true on the validator. Default jsonschema ignores additionalProperties if you use the wrong meta-schema.

AI agent tool schema design is defense in depth. The schema is the first wall, validation is the second, and multi-model testing is the moat. Build all three and your agent will call tools correctly even when the prompt gets weird.

Tagsai-agentstool-useschema-designtool-definitions

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All ai agent tool use design patterns posts →