> ## 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
`NANNY_API_KEY`, 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 --serve > 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,
  "run_id": "a1b2c3d4",
  "seq": 0,
  "command": "python agent.py",
  "allowed_tools": ["web_search", "send_outreach"],
  "tool_labels": {
    "web_search": ["reads_untrusted"],
    "send_outreach": ["external_effect"]
  },
  "config_hash": "9f2a41c8"
}
```

### ExecutionStopped

Always the last event of a complete run. Emitted on every exit path: clean exit, a policy stop, an error, or a signal.

```json theme={null}
{
  "event": "ExecutionStopped",
  "ts": 1711234572000,
  "run_id": "a1b2c3d4",
  "seq": 12,
  "reason": "AgentCompleted",
  "tokens_spent": 70,
  "elapsed_ms": 4823
}
```

If this event is missing from a run, the process crashed. That absence is itself
a fact worth reading.

`reason` is one of four: `ToolDenied`, `RuleDenied`, `AgentCompleted`, or
`ManualStop`. Only the first two are policy violations.

## 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                                                                 |
| ------------------- | ---------------------------------------------------------------------------- |
| `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                            |
| `RulesDeclared`     | Once, when the agent declares which rules it registered                      |

`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 hit a runtime error. No tokens are recorded for a failure.

## Using the log

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

```bash theme={null}
# Every event for one run, in order
cat nanny.log | jq -c 'select(.run_id == "a1b2c3d4")' | sort -t: -k3 -n

# 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'
```
