# Tools and their parameters

A tool is a configured, named instance of something the platform offers: a
built-in, a tool imported from an MCP server, a flow, or custom code. This page
is about the built-ins and the parameters they accept, because two facts about
them surprise everyone who meets them through the API for the first time:

- **An instance's parameters are not a copy taken when it was created.** They
  are the deployment's current ones. A parameter the platform added last week
  is accepted by an instance created last year, and nothing is re-created or
  refreshed to get it.
- **`fixed_parameters` on a `PATCH` replaces the whole set.** It does not merge.
  Sending the one key you want to add silently unfixes the rest.

The calendar built-ins are the running example throughout.

## Discovering the built-ins

```bash
curl https://YOUR_PLATFORM_ORIGIN/api/v1/tools/builtins \
  -H "Authorization: Bearer $ATTEQ_API_KEY"
```

One entry per built-in this deployment runs. The answer describes the
deployment, not your rows: it is the same for every organization, less any
built-in that needs a feature your key does not carry. This is the read to make
before creating anything, and the only thing that can say what a built-in
accepts *today*.

One entry, in full:

```json
{
  "name": "calendar_create_event",
  "input_schema": {
    "type": "object",
    "properties": {
      "start": {"type": "string", "description": "When the event starts."},
      "end": {"type": "string", "description": "When the event ends."},
      "summary": {"type": "string", "description": "The event's title."},
      "description": {"type": "string", "description": "Free text on the event."},
      "location": {
        "type": "string",
        "description": "Where the event is: an address, or coordinates as 'lat,lng'. Kept exactly as given."
      },
      "attendees": {
        "type": "array",
        "items": {"type": "string"},
        "description": "Email addresses to invite, exactly as the caller gave them."
      },
      "refuse_overlap": {
        "type": "boolean",
        "description": "Optional: true refuses a slot that overlaps anything already in the calendar, naming what it collides with; false (the default) writes the slot as asked."
      },
      "calendar_id": {
        "type": "string",
        "description": "The calendar within the connected account. Fixed on the instance."
      },
      "timezone": {
        "type": "string",
        "description": "The IANA timezone busy ranges are expressed in and naive instants are read in, e.g. Europe/Madrid. Fixed on the instance."
      }
    },
    "required": ["start", "end", "summary", "calendar_id", "timezone"],
    "additionalProperties": false
  },
  "fixed_parameters": ["calendar_id", "timezone"],
  "provider": "google",
  "required_flag": null
}
```

| Field | What it is |
|---|---|
| `name` | The built-in's name, and what an instance of it carries in `config.builtin` when you create it. |
| `input_schema` | Every parameter the built-in accepts today, the keys a person fixes included. This is the schema the instance has, not a template to copy and edit. |
| `fixed_parameters` | A **list of key names**, not values: the parameters a person is expected to fill on the instance, because they decide what the instance *is* rather than what one call does. Here, which calendar and which timezone. |
| `provider` | The connection provider an instance needs — `google` for the calendars — or `null` for a built-in that needs no authorization of yours. |
| `required_flag` | The feature an organization must have to make an instance of it, or `null`. Entries naming a feature your key does not carry are absent from the answer altogether, so what you read here is what you may create. |

An entry with `input_schema: null` is a built-in no client creates by hand — the
three a collection brings with it (`custom_data_query`, `custom_data_get`,
`custom_data_write`, which appear in `list_tools` as `query_<slug>`,
`get_<slug>` and `write_<slug>` the moment the collection exists). It is listed
because the deployment runs it, and there is nothing to configure on it.

## The five calendar built-ins

All five fix `calendar_id` and `timezone`, and all five point at a `google`
connection. Which of them that connection backs depends on what the person
authorized: `see_busy` backs the busy read and nothing else, `see_appointments`
backs the listing and no write, `manage_events` backs the listing and every
write. A client who wants both reads grants both — the busy read is its own
permission at Google too.

| Built-in | What it does | Parameters besides the fixed two |
|---|---|---|
| `calendar_read_busy` | The busy ranges between two instants, and nothing computed on top: no free slots, no "next available". | `time_min` and `time_max` together, or `days` alone (1–31) from the moment of the call; never both forms. |
| `calendar_list_events` | Each event between two instants as the diary holds it — start, end, title, location, its identifier, whether the platform created it — the person's own entries included. | The same interval as the busy read. |
| `calendar_create_event` | Writes the event it was given. It does not check that the slot fits unless `refuse_overlap` says so. | `start`, `end`, `summary` (required), `description`, `location`, `attendees`, `refuse_overlap`. |
| `calendar_update_event` | Replaces what it is given and leaves the rest as it was: a name learnt after booking, a visit moved, a second person invited. | `event_id` (required), then any of `start`, `end`, `summary`, `description`, `location`, `attendees`, `refuse_overlap`. Moving an event gives both `start` and `end`; a start alone would leave the length to a guess. |
| `calendar_cancel_event` | Removes the event, and is its own action — so an agent can be allowed to change visits without being allowed to cancel them. | `event_id`. |

An event is changed or cancelled only if the platform created it in that same
calendar; one a person put there answers exactly as one that does not exist. The
identifier is the one `calendar_create_event` answered with.

### `refuse_overlap`: whether a write checks the slot first

A create or an update can refuse a slot that overlaps anything already in the
diary, and **the refusal names the ranges it collides with**, so the model can
offer another time instead of booking on top of a visit.

**It is off unless it is turned on**, because some clients overlap on purpose.
It is a parameter of the two write tools like any other — not a setting of its
own — so a client turns it on by fixing `refuse_overlap: true` on the instance;
left unfixed, it is the model's to pass or not. It is a read just before the
write and not a lock, so two writes at the same instant may still both land.

### `location`: kept exactly as it was given

Where the event is, as an address or as coordinates written `lat,lng`. It is
stored as given and read back the same way: nothing is geocoded, reformatted or
looked up. `calendar_list_events` returns it as part of each event, and an
update replaces it.

## Creating an instance

```bash
curl -X POST https://YOUR_PLATFORM_ORIGIN/api/v1/tools \
  -H "Authorization: Bearer $ATTEQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "book_visit",
    "description": "Put a visit in the diary.",
    "source": "builtin",
    "config": {"builtin": "calendar_create_event"},
    "fixed_parameters": {"calendar_id": "primary", "timezone": "Europe/Madrid"},
    "connection_id": "8f1c0e6a-5c2e-4f1e-9c2a-0b7d5a3e1f44"
  }'
```

Three things about that body:

- **`config.builtin` is what makes it an instance of this built-in**, exactly as
  `config.flow_id` is what makes a tool a flow. Spell the name as
  `list_builtins` gave it; a name this deployment does not run reaches nothing.
- **No `input_schema`.** What a built-in accepts is the platform's to decide, so
  there is nothing to declare: the instance has the deployment's schema whatever
  you send.
- **`connection_id` and never a credential.** The Google connection is
  authorized by the person whose diary it is, through Google's consent screen,
  and `list_connections` gives you its id; its `capabilities` say which of the
  five tools it backs. A tool's configuration that carried a key itself is
  refused when it is created or changed.

The answer is the instance. A tool's `config` is never returned — it is the
place credentials can live — which is why `builtin` and `connection_id` are
published as their own fields:

```json
{
  "id": "3b9f2c14-7a6e-4f0b-9a83-2c0d61e4a7bd",
  "org_id": "0a6c1f33-82d4-4b55-8e01-7d9f4c2b6a10",
  "name": "book_visit",
  "description": "Put a visit in the diary.",
  "source": "builtin",
  "builtin": "calendar_create_event",
  "fixed_parameters": {"calendar_id": "primary", "timezone": "Europe/Madrid"},
  "deadline_seconds": 30,
  "conversation_only": false,
  "requires_confirmation": false,
  "connection_id": "8f1c0e6a-5c2e-4f1e-9c2a-0b7d5a3e1f44",
  "connection_group": null,
  "mcp_server": null,
  "owner": null,
  "holds_credential": false
}
```

The answer carries one more field, elided above for length: `input_schema`, the
built-in's current schema — the same object the catalogue listed. Why it is the
current one, and not the one that existed when the row was written, is the next
section.

## A flow as a tool

A [flow](/how-it-fits) is offered to an agent as one tool by an instance with
`source` `"builtin"` and the flow's id in `config.flow_id`. Nothing else goes
in `config`: no `endpoint`, no `connection_id`, no header. The platform runs the
flow itself, so there is no address to call it at, and an HTTP tool pointed at
this platform is not a way to run one.

```bash
curl -X POST https://YOUR_PLATFORM_ORIGIN/api/v1/tools \
  -H "Authorization: Bearer $ATTEQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "check_order",
    "description": "Look up the status of an order by its number.",
    "source": "builtin",
    "config": {"flow_id": "FLOW_ID"},
    "input_schema": {
      "type": "object",
      "properties": {"order_number": {"type": "string"}},
      "required": ["order_number"]
    }
  }'
```

- **`input_schema` is yours to write.** It is what the agent fills in when it
  calls the flow, and the flow's steps read each value as
  `$ref:trigger.<key>` (here `$ref:trigger.order_number`). A flow that needs
  nothing from the conversation takes `{"type": "object", "properties": {}}`.
- **The answer's `id` is what an agent carries**: in `global_tool_ids` on the
  draft (`edit_draft`) for every situation, or in a node's `tool_ids`
  (`update_node`) for one.
- **Testing the instance runs the flow**, every step, with the parameters you
  send: `POST /api/v1/tools/{id}/test` with `{"parameters": {"order_number": "A-1"}}`.

## An instance accepts what the built-in accepts today

```bash
curl https://YOUR_PLATFORM_ORIGIN/api/v1/tools/$TOOL_ID \
  -H "Authorization: Bearer $ATTEQ_API_KEY"
```

A built-in instance's parameters are the deployment's current ones. What a
built-in accepts is the platform's to decide and it changes when the platform
does, so an instance created months ago accepts — and offers the model — what
that built-in accepts now, less what you fixed on it. **Nobody re-creates or
refreshes an instance to receive a parameter the platform added.** `location`
and `refuse_overlap` were both added to the calendar writes after the first
instances of those tools existed, and they work on every one of them.

Two consequences worth stating plainly, because both have been got wrong:

- **An instance is not evidence about what a built-in accepts.** If the
  parameter you are looking for is not where you expected it on a tool row,
  read `GET /api/v1/tools/builtins` — the deployment's own answer — before
  concluding it does not exist.
- **A value fixed on a parameter the built-in no longer has stays on the
  instance and reaches nothing.** It is not an error and it is not applied; it
  is a leftover to clean up.

`fixed_parameters` on the instance says which keys a person fixed, and the
model is shown the schema **less those keys**: the instance always says which
values are fixed on it.

## Fixing a parameter

A parameter fixed on the instance by a person is absent from what the model is
shown. The model does not know it exists, cannot ask for it and cannot
overwrite it — which is the whole point for `calendar_id` and `timezone`: a
model that could name the calendar could name the wrong one, and one that could
pick the timezone would be an hour out on a schedule nobody could audit. The
record of the conversation shows what each run actually ran with, fixed values
included, so a fixed value is hidden from the model without being hidden from
you.

Any declared parameter of any instance can be fixed or left to the model, one
parameter at a time, from the instance's own schema. Turning the overlap check
on is therefore not a special call — it is fixing `refuse_overlap` on the
instance. Which brings us to the one trap on this page.

## The trap: `fixed_parameters` replaces the whole set

`PATCH /api/v1/tools/{tool_id}` is a partial edit: only the fields you send
change. But `fixed_parameters` is one field whose value is an object, and **the
object you send replaces the stored one whole**. What your change does not name
is no longer fixed and returns to the model.

So this call, which looks like "also fix the overlap check":

```json
{"fixed_parameters": {"refuse_overlap": true}}
```

is accepted, and it leaves the instance fixing `refuse_overlap` and nothing
else. `calendar_id` and `timezone` are no longer fixed, so the model is now
shown both and asked to name them itself — and since the schema requires them,
the next call either guesses a calendar or fails outright.

The right call reads the instance, merges, and sends the whole set back:

```bash
curl https://YOUR_PLATFORM_ORIGIN/api/v1/tools/$TOOL_ID \
  -H "Authorization: Bearer $ATTEQ_API_KEY"
```

```bash
curl -X PATCH https://YOUR_PLATFORM_ORIGIN/api/v1/tools/$TOOL_ID \
  -H "Authorization: Bearer $ATTEQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fixed_parameters": {
      "calendar_id": "primary",
      "timezone": "Europe/Madrid",
      "refuse_overlap": true
    }
  }'
```

Unfixing a parameter is the same call without it: there is no "remove this key"
operation, because the set you send *is* the set that is fixed. Every other
field of `update_tool` behaves as you would expect — a `name` alone changes the
name and touches nothing else.

> **Read, merge, write, every time you touch `fixed_parameters`.** Two people
> editing one instance an hour apart, each sending only their own key, leave it
> fixing whatever the second one sent.

## If you are a model reading this

1. **Read the catalogue, then create.** `GET /api/v1/tools/builtins` names every
   built-in, its current parameters, the keys a person fixes and the connection
   provider it needs. Guessing a parameter name or copying one out of an older
   instance is how an instance gets created that the runner then refuses.
2. **Do not send `input_schema` for a built-in.** It is the platform's, not
   yours; send `source: "builtin"` and `config.builtin` and leave the schema
   alone.
3. **Never report that a parameter does not exist because one tool row does not
   mention it.** The catalogue is the answer; a row is one client's
   configuration of it.
4. **Treat `fixed_parameters` as read-modify-write.** A `PATCH` that names it
   replaces it.
5. **A tool's `config` is never returned.** If you need to know which built-in
   an instance is, read `builtin`; for its authorization, `connection_id` or
   `connection_group`.

## Where next

- [How it fits together](/how-it-fits) — which pieces a job needs, and the
  words the API uses for each.
- [Book an appointment without double-booking](/recipes/book-appointment) — a
  booking built end to end, as the sequence of operations it is, run as a test
  against the API before it was published.
- [API Reference](/api) — `list_builtins`, `create_tool`, `get_tool`,
  `update_tool` and `test_tool`, with their schemas.
