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

Events go to **stdout**. Nanny's own output, the startup block and any warning,
goes to stderr, so redirecting stdout gives you the event log by itself:

```bash theme={null}
nanny run > events.ndjson
```

Your agent's own stdout still passes through, since it inherits the stream, so
a consumer that wants only events filters for lines beginning with `{`.

In a container, redirect nothing. The runtime collects stdout already and every
log shipper reads it from there. A Datadog agent, for example, needs no Nanny
configuration at all:

```yaml theme={null}
labels:
  com.datadoghq.ad.logs: '[{"source":"nanny","service":"your-app"}]'
```

If you do want a file inside a container, redirect in the entrypoint rather
than piping, so Nanny stays PID 1 and still receives `SIGTERM`:

```sh theme={null}
exec nanny run > /var/log/nanny/events.ndjson    # redirection survives exec
exec nanny run | tee /var/log/nanny/events.ndjson # a pipe does not: avoid
```

**Durable delivery is Cloud's job, not the log's.** Nothing on disk survives a
container that is replaced. When `NANNY_API_KEY` is set, undelivered batches
are held in `.nanny/spool/` and sent on the next run, which is the path worth
mounting a volume for. See [Connect to Nanny Cloud](/v0.7/guides/managed-mode).

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