> ## Documentation Index
> Fetch the complete documentation index at: https://graph-unify-model-roles.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# User-defined tools

> Wrap commands, queries, and prompts as first-class tools

Drop a YAML file in `./.graph/tools/` (or `~/.config/graph/tools/` for tools you want everywhere) and it becomes `user__<name>` — callable by the agent, referenced in plan steps or from the CLI.

## `exec` — wrap any command

```yaml theme={null}
name: git_log
description: Recent git commits for a local repository path.
kind: exec
command: git
args:
  - "-C"
  - "{{input.repo}}"
  - "log"
  - "-n"
  - "{{input.count}}"
  - "--pretty=format:{\"hash\":\"%h\",\"subject\":\"%s\"},"
output: text                # or json: stdout parsed as JSON
read_only: true
input_schema:
  type: object
  required: [repo]
  properties:
    repo: { type: string, description: Absolute path to a git repository }
    count: { type: integer }
```

| Field          | Required | Notes                                                                                     |
| -------------- | -------- | ----------------------------------------------------------------------------------------- |
| `command`      | yes      | the executable                                                                            |
| `args`         | no       | templated from the input (`{{input.*}}` only — step references don't exist inside a tool) |
| `env`          | no       | extra environment; values support `${VAR}` from the parent environment                    |
| `cwd`          | no       | working directory for the process                                                         |
| `timeout_secs` | no       | kill-after budget; default 60                                                             |
| `output`       | no       | `text` (default: stdout wrapped as `{"text": …}`) or `json` (stdout parsed as JSON)       |

Unparseable `json` output, non-zero exit (with stderr captured), and timeouts all return as structured tool errors.

<Warning>
  Exec tools are **arbitrary code execution, by design** — you author them, graph runs them. Treat the tools directory like you treat your shell profile.
</Warning>

## `prompt` — an LLM call as a tool

```yaml theme={null}
name: summarize
description: Summarize any text into a gist and three keywords.
kind: prompt
prompt: |
  Summarize into a single-sentence gist and exactly three keywords:

  {{input.text}}
model: chat                  # any configured role: standard (chat, solver, …) or custom
output_schema:               # optional → enforced structured output
  type: object
  required: [gist, keywords]
  properties:
    gist: { type: string }
    keywords: { type: array, items: { type: string } }
```

With an `output_schema`, the result is validated JSON; without one, `{"text": …}`. Useful as a cheap sub-task inside plans — classify, extract, reword — with `model` controlling cost: any configured [role](/models/models-and-providers#roles) — a standard one like `chat` (the default, with the usual fallback to `default`) or a [custom one](/models/models-and-providers#custom-roles) like `nano`. An unknown name fails the call with the configured names listed.

## Shared behavior

* `input_schema` validates before dispatch — missing fields return actionable errors (in chat, the agent asks and retries).
* `output_schema`, when declared, feeds the planner's shape knowledge just like an MCP output schema.
* Names must match `[a-zA-Z0-9_-]+`; templates referencing anything but `{{input.*}}` are load-time errors.
* Files are read strictly: a key the tool's `kind` does not accept is a load error naming it (`unknown field(s) \`tmeout\_secs\` for a \`exec\` tool\`), never ignored.
* `version` is the [file version](/reference/file-versions#plans-and-tools) the document is written in; omitted means `1`. `graph tools migrate <path>` brings a file to the current version.
