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

# Event Log

> Every Nanny execution emits a structured NDJSON event log. Here's what's in it.

## Format

The event log is **NDJSON** — one JSON object per line, emitted in chronological order.
Every object has an `"event"` field identifying its type and a `"ts"` field with a Unix
timestamp in milliseconds.

## Output destination

By default, events are written to **stdout**, interleaved with your agent's own output.
To separate them, configure file output:

```toml theme={null}
[observability]
log = "file"
```

This writes to `.nanny/logs/log.ndjson`, a location Nanny always owns and auto-creates,
never something you point at a path yourself. Only set `file` if you want a different
name — a bare name, no extension, Nanny always appends `.ndjson` itself:

```toml theme={null}
[observability]
log  = "file"
file = "events"   # writes .nanny/logs/events.ndjson
```

`.nanny/logs/` is meant to stay out of git (Nanny adds it to `.gitignore` automatically
the first time it's created): it's a local audit trail, not source. If you later log this
machine in (`nanny auth login`), Cloud sync can back-fill from whatever accumulated in
that folder before sync was ever turned on, not just events going forward.

Or pipe stdout to a file at the shell level:

```bash theme={null}
nanny run > nanny.log
```

## Guaranteed events

Every execution emits exactly these two events, in this order:

### ExecutionStarted

Always the first event. Emitted immediately before the child process is spawned.

```json theme={null}
{
  "event": "ExecutionStarted",
  "ts": 1711234567000,
  "limits_set": "[limits]",
  "command": "python agent.py",
  "limits": {
    "steps": 100,
    "tokens": 1000,
    "timeout": 30000
  }
}
```

### ExecutionStopped

Always the last event. Emitted on every exit path — clean exit, timeout, error, or signal.

```json theme={null}
{
  "event": "ExecutionStopped",
  "ts": 1711234572000,
  "reason": "AgentCompleted",
  "steps": 7,
  "tokens_spent": 70,
  "elapsed_ms": 4823
}
```

If the process was killed, `reason` will be one of the stop reasons listed in
[Limits & Enforcement](/v0.5/concepts/limits).

## SDK events

When the Rust SDK macros or Python SDK decorators are active, additional events are emitted for each tool call. These appear between `ExecutionStarted` and `ExecutionStopped`:

| Event               | When emitted                                                                 |
| ------------------- | ---------------------------------------------------------------------------- |
| `StepCompleted`     | After each allowed tool call, immediately following its `ToolAllowed` event  |
| `ToolAllowed`       | When Nanny permits a tool call                                               |
| `ToolDenied`        | When a tool call is blocked because the tool is not in the allowlist         |
| `RuleDenied`        | When a tool call is blocked by a custom rule or a per-tool `max_calls` limit |
| `ToolFailed`        | When a permitted tool fails at runtime (network error, bad args, etc.)       |
| `AgentScopeEntered` | When a `#[nanny::agent("name")]` function is entered                         |
| `AgentScopeExited`  | When a `#[nanny::agent("name")]` function returns                            |

`ToolDenied` and `RuleDenied` are distinct denial events — `ToolDenied` means the tool was not permitted at all; `RuleDenied` means the tool was permitted but a rule blocked this specific call.
`ToolFailed` is different from both — the tool was allowed and called, but encountered a runtime error. No tokens are charged on failure.

## Using the log

The event log is designed to be piped into standard tools:

```bash theme={null}
# Count total steps (every allowed tool call is one step)
cat nanny.log | grep StepCompleted | wc -l

# Find all denied tool calls (allowlist blocks and rule denials)
cat nanny.log | jq 'select(.event == "ToolDenied" or .event == "RuleDenied")'

# Check why a run stopped
cat nanny.log | jq 'select(.event == "ExecutionStopped") | .reason'
```
