> ## Documentation Index
> Fetch the complete documentation index at: https://docs.engini.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Scope an agent with a toolset

> Decide exactly which tools an agent may call, and which connection each one runs through.

<Info>**Uses:** Python · TypeScript · CLI · REST API</Info>

Handing an LLM every tool in your account is how agents send email from the wrong mailbox. A **toolset** is the guardrail: a named bundle that pins which connection each application uses and which tools are permitted.

Once it exists, pass its id on discovery and execution - the server enforces the boundary, so a model that hallucinates a tool name simply gets rejected.

<Note>
  cURL samples assume `BASE=https://api.engini.io/v1` and `AUTH="x-api-key: $ENGINI_API_KEY"` - the setup from the [REST walkthrough](/examples/rest-walkthrough).
</Note>

## Create one

<CodeGroup>
  ```python Python theme={null}
  toolset = client.toolsets.create(
      name="Support agent",
      connections=[
          {"connectionId": 42, "toolSlugs": ["gmail_send_mail", "gmail_list_messages"]},
          {"connectionId": 17, "applicationSlug": "monday"},
      ],
  )
  ```

  ```typescript TypeScript theme={null}
  const toolset = await client.toolsets.create({
    name: "Support agent",
    connections: [
      { connectionId: 42, toolSlugs: ["gmail_send_mail", "gmail_list_messages"] },
      { connectionId: 17, applicationSlug: "monday" },
    ],
  });
  ```

  ```bash CLI theme={null}
  engini toolsets create --name "Support agent" \
    --connection 42:gmail_send_mail,gmail_list_messages \
    --connection 17
  ```

  ```bash cURL theme={null}
  curl -s -X POST "$BASE/toolsets" -H "$AUTH" -H "Content-Type: application/json" -d '{
    "name": "Support agent",
    "connections": [
      { "connectionId": 42, "toolSlugs": ["gmail_send_mail", "gmail_list_messages"] },
      { "connectionId": 17, "applicationSlug": "monday" }
    ]
  }'
  ```
</CodeGroup>

<Note>
  The CLI manages toolsets too - `engini toolsets list | get | create | update | delete`, see the [command reference](/cli/commands#toolsets). `engini tools list --toolset <id>` shows exactly what a toolset permits, which is the fastest way to confirm a scope is what you intended - and `engini toolsets update <id> --dry-run` previews a change before anything is written.
</Note>

Two entry styles, and the difference matters:

* **`toolSlugs` listed** - only those tools are callable. Use this for anything destructive.
* **`applicationSlug` with no `toolSlugs`** - every tool of that application is permitted. Convenient, broader.

You can also omit `connectionId` and let the server resolve your default connection for the application at save time - it stores the concrete id, so the binding doesn't drift later.

## Use it

Pass `toolsetId` on both discovery and execution:

<CodeGroup>
  ```python Python theme={null}
  ts = client.toolset(toolset_id=toolset_id)
  tools = ts.tools()                        # discovery: only the permitted tools

  result = client.tools.execute(
      "gmail_send_mail",
      {"to": "alex@acme.com", "subject": "Hi", "body": "..."},
      toolset_id=toolset_id,                # execution: boundary enforced server-side
  )
  ```

  ```typescript TypeScript theme={null}
  const ts = await client.toolset({ toolsetId });
  const tools = await ts.tools();           // discovery: only the permitted tools

  const result = await client.tools.execute(
    "gmail_send_mail",
    { to: "alex@acme.com", subject: "Hi", body: "..." },
    { toolsetId },                          // execution: boundary enforced server-side
  );
  ```

  ```bash CLI theme={null}
  engini tools list --toolset $TOOLSET_ID   # discovery: only the permitted tools
  engini tools call gmail_send_mail --toolset $TOOLSET_ID \
    --args '{"to":"alex@acme.com","subject":"Hi","body":"..."}'
  ```

  ```bash cURL theme={null}
  curl -s "$BASE/tools?toolsetId=$TOOLSET_ID" -H "$AUTH" | jq -r '.items[].toolSlug'

  curl -s -X POST "$BASE/tools/gmail_send_mail/execute?toolsetId=$TOOLSET_ID" \
    -H "$AUTH" -H "Content-Type: application/json" \
    -d '{"fields":{"to":"alex@acme.com","subject":"Hi","body":"..."}}'
  ```
</CodeGroup>

Discovery returns only the permitted tools, so the model never learns that anything else exists - the cheapest guardrail there is. Execution then enforces it server-side:

| Attempt                                   | Result                          |
| ----------------------------------------- | ------------------------------- |
| Tool outside the allow-list               | `403 TOOL_NOT_IN_TOOLSET`       |
| `connectionId` not in the toolset         | `403 CONNECTION_NOT_IN_TOOLSET` |
| No connection for that app in the toolset | `409 NO_TOOLSET_CONNECTION`     |

## In an agent loop

The SDK wraps all of this - see [Toolsets](/sdk/toolsets) and the [monday assistant](/examples/monday-agent). (This part is SDK-only by nature: the agent loop lives in your code.)

<CodeGroup>
  ```python Python theme={null}
  toolset = client.toolset(toolset_id=toolset_id)
  tools = toolset.tools()                      # only the permitted tools, vendor-shaped
  results = toolset.handle_tool_calls(reply)   # execution stays inside the boundary
  ```

  ```typescript TypeScript theme={null}
  const toolset = await client.toolset({ toolsetId });
  const tools = await toolset.tools();                    // only the permitted tools, vendor-shaped
  const results = await toolset.handleToolCalls(reply);   // execution stays inside the boundary
  ```
</CodeGroup>

## Managing them

<CodeGroup>
  ```python Python theme={null}
  all_sets = client.toolsets.list()
  client.toolsets.update(toolset_id, connections=[
      {"connectionId": 42, "toolSlugs": ["gmail_list_messages"]},
  ])
  client.toolsets.delete(toolset_id)
  ```

  ```typescript TypeScript theme={null}
  const allSets = await client.toolsets.list();
  await client.toolsets.update(toolsetId, {
    connections: [{ connectionId: 42, toolSlugs: ["gmail_list_messages"] }],
  });
  await client.toolsets.delete(toolsetId);
  ```

  ```bash CLI theme={null}
  engini toolsets list
  engini toolsets update $ID --connection 42:gmail_list_messages --dry-run   # preview the diff first
  engini toolsets update $ID --connection 42:gmail_list_messages
  engini toolsets delete $ID
  ```

  ```bash cURL theme={null}
  curl -s "$BASE/toolsets" -H "$AUTH" | jq -r '.items[] | "\(.toolsetId)  \(.name)"'
  curl -s -X PATCH "$BASE/toolsets/$ID" -H "$AUTH" -H "Content-Type: application/json" \
    -d '{"connections":[{"connectionId":42,"toolSlugs":["gmail_list_messages"]}]}'
  curl -s -X DELETE "$BASE/toolsets/$ID" -H "$AUTH"
  ```
</CodeGroup>

<Warning>
  `PATCH` replaces `connections` and `workflows` wholesale when you supply them - it does not merge. Send the full intended set, or omit the field to leave it untouched.
</Warning>

## A pattern worth stealing

Give each agent **its own** toolset rather than sharing one, and make read-only agents literally incapable of writing:

```json theme={null}
{ "name": "Reporting agent (read-only)",
  "connections": [{ "connectionId": 42, "toolSlugs": ["crm_get_records", "crm_search"] }] }
```

Now "please delete the duplicates" fails at the API boundary instead of depending on the model declining. That's a control you can point at during a security review.
