From a2b6fdc3df110a5f49c11f4de74f23414ad8bc54 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 3 Apr 2026 13:11:13 +0900 Subject: [PATCH] docs: rebuild automation section for coherence and readability --- docs/automation/cron-jobs.md | 121 +-- docs/automation/hooks.md | 1339 ++++------------------------------ docs/automation/index.md | 50 +- docs/automation/taskflow.md | 47 +- docs/docs.json | 6 +- 5 files changed, 240 insertions(+), 1323 deletions(-) diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index a9938f0d58..a113f8de75 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -37,9 +37,7 @@ openclaw cron runs --id - All cron executions create [background task](/automation/tasks) records. - One-shot jobs (`--at`) auto-delete after success by default. -## Adding jobs - -### Schedule types +## Schedule types | Kind | CLI flag | Description | | ------- | --------- | ------------------------------------------------------- | @@ -51,7 +49,35 @@ Timestamps without a timezone are treated as UTC. Add `--tz America/New_York` fo Recurring top-of-hour expressions are automatically staggered by up to 5 minutes to reduce load spikes. Use `--exact` to force precise timing or `--stagger 30s` for an explicit window. -### CLI examples +## Execution styles + +| Style | `--session` value | Runs in | Best for | +| --------------- | ------------------- | ------------------------ | ------------------------------- | +| Main session | `main` | Next heartbeat turn | Reminders, system events | +| Isolated | `isolated` | Dedicated `cron:` | Reports, background chores | +| Current session | `current` | Bound at creation time | Context-aware recurring work | +| Custom session | `session:custom-id` | Persistent named session | Workflows that build on history | + +**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries. + +### Payload options for isolated jobs + +- `--message`: prompt text (required for isolated) +- `--model` / `--thinking`: model and thinking level overrides +- `--light-context`: skip workspace bootstrap file injection +- `--tools exec,read`: restrict which tools the job can use + +## Delivery and output + +| Mode | What happens | +| ---------- | -------------------------------------------------------- | +| `announce` | Deliver summary to target channel (default for isolated) | +| `webhook` | POST finished event payload to a URL | +| `none` | Internal only, no delivery | + +Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:`, `user:`). + +## CLI examples One-shot reminder (main session): @@ -92,40 +118,6 @@ openclaw cron add \ --announce ``` -## Execution styles - -| Style | `--session` value | Runs in | Best for | -| --------------- | ------------------- | ------------------------ | ------------------------------- | -| Main session | `main` | Next heartbeat turn | Reminders, system events | -| Isolated | `isolated` | Dedicated `cron:` | Reports, background chores | -| Current session | `current` | Bound at creation time | Context-aware recurring work | -| Custom session | `session:custom-id` | Persistent named session | Workflows that build on history | - -**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). They use `payload.kind = "systemEvent"`. - -**Isolated** jobs run a dedicated agent turn. Each run starts a fresh session (no carry-over) unless using a custom session. Default delivery is `announce` (summary to chat). - -**Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries. - -### Payload options for isolated jobs - -- `--message`: prompt text (required for isolated) -- `--model` / `--thinking`: model and thinking level overrides -- `--light-context`: skip workspace bootstrap file injection -- `--tools exec,read`: restrict which tools the job can use - -## Delivery and output - -| Mode | What happens | -| ---------- | -------------------------------------------------------- | -| `announce` | Deliver summary to target channel (default for isolated) | -| `webhook` | POST finished event payload to a URL | -| `none` | Internal only, no delivery | - -Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. - -For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:`, `user:`). - ## Webhooks Gateway can expose HTTP webhook endpoints for external triggers. Enable in config: @@ -247,13 +239,6 @@ gog gmail watch start \ } ``` -### Test - -```bash -gog gmail send --account openclaw@gmail.com --to openclaw@gmail.com --subject "watch test" --body "ping" -gog gmail watch status --account openclaw@gmail.com -``` - ## Managing jobs ```bash @@ -280,33 +265,6 @@ openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --mes openclaw cron edit --clear-agent ``` -## JSON schema for tool calls - -One-shot main session job: - -```json -{ - "name": "Reminder", - "schedule": { "kind": "at", "at": "2026-02-01T16:00:00Z" }, - "sessionTarget": "main", - "wakeMode": "now", - "payload": { "kind": "systemEvent", "text": "Reminder text" }, - "deleteAfterRun": true -} -``` - -Recurring isolated job with delivery: - -```json -{ - "name": "Morning brief", - "schedule": { "kind": "cron", "expr": "0 7 * * *", "tz": "America/Los_Angeles" }, - "sessionTarget": "isolated", - "payload": { "kind": "agentTurn", "message": "Summarize overnight updates." }, - "delivery": { "mode": "announce", "channel": "slack", "to": "channel:C1234567890" } -} -``` - ## Configuration ```json5 @@ -329,16 +287,11 @@ Recurring isolated job with delivery: Disable cron: `cron.enabled: false` or `OPENCLAW_SKIP_CRON=1`. -### Retry policy +**One-shot retry**: transient errors (rate limit, overload, network, server error) retry up to 3 times with exponential backoff. Permanent errors disable immediately. -**One-shot jobs**: retry transient errors (rate limit, overload, network, server error) up to 3 times with exponential backoff. Permanent errors disable immediately. +**Recurring retry**: exponential backoff (30s to 60m) between retries. Backoff resets after the next successful run. -**Recurring jobs**: exponential backoff (30s to 60m) between retries. Backoff resets after the next successful run. - -### Maintenance - -- `cron.sessionRetention` (default `24h`): prune isolated run-session entries. -- `cron.runLog.maxBytes` / `cron.runLog.keepLines`: auto-prune run-log files. +**Maintenance**: `cron.sessionRetention` (default `24h`) prunes isolated run-session entries. `cron.runLog.maxBytes` / `cron.runLog.keepLines` auto-prune run-log files. ## Troubleshooting @@ -364,16 +317,10 @@ openclaw doctor ### Cron fired but no delivery -- Run succeeded but delivery mode is `none` means no external message is expected. +- Delivery mode is `none` means no external message is expected. - Delivery target missing/invalid (`channel`/`to`) means outbound was skipped. - Channel auth errors (`unauthorized`, `Forbidden`) mean delivery was blocked by credentials. -### Heartbeat suppressed or skipped - -- `reason=quiet-hours`: outside `activeHours`. -- `requests-in-flight`: main lane busy, heartbeat deferred. -- `empty-heartbeat-file`: `HEARTBEAT.md` has no actionable content and no cron event is queued. - ### Timezone gotchas - Cron without `--tz` uses the gateway host timezone. diff --git a/docs/automation/hooks.md b/docs/automation/hooks.md index ef4197ea03..71a239eb51 100644 --- a/docs/automation/hooks.md +++ b/docs/automation/hooks.md @@ -8,92 +8,54 @@ title: "Hooks" # Hooks -Hooks provide an extensible event-driven system for automating actions in response to agent commands and events. Hooks are automatically discovered from directories and can be inspected with `openclaw hooks`, while hook-pack installation and updates now go through `openclaw plugins`. +Hooks are small scripts that run when something happens inside the Gateway. They are automatically discovered from directories and can be inspected with `openclaw hooks`. -## Getting Oriented +There are two kinds of hooks in OpenClaw: -Hooks are small scripts that run when something happens. There are two kinds: +- **Internal hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events. +- **Webhooks**: external HTTP endpoints that let other systems trigger work in OpenClaw. See [Webhooks](/automation/cron-jobs#webhooks). -- **Hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events. -- **Webhooks**: external HTTP webhooks that let other systems trigger work in OpenClaw. See [Webhooks](/automation/cron-jobs#webhooks) or use `openclaw webhooks` for Gmail helper commands. +Hooks can also be bundled inside plugins. `openclaw hooks list` shows both standalone hooks and plugin-managed hooks. -Hooks can also be bundled inside plugins; see [Plugin hooks](/plugins/architecture#provider-runtime-hooks). `openclaw hooks list` shows both standalone hooks and plugin-managed hooks. - -Common uses: - -- Save a memory snapshot when you reset a session -- Keep an audit trail of commands for troubleshooting or compliance -- Trigger follow-up automation when a session starts or ends -- Write files into the agent workspace or call external APIs when events fire - -If you can write a small TypeScript function, you can write a hook. Managed and bundled hooks are trusted local code. Workspace hooks are discovered automatically, but OpenClaw keeps them disabled until you explicitly enable them via the CLI or config. - -## Overview - -The hooks system allows you to: - -- Save session context to memory when `/new` is issued -- Log all commands for auditing -- Trigger custom automations on agent lifecycle events -- Extend OpenClaw's behavior without modifying core code - -## Getting Started - -### Bundled Hooks - -OpenClaw ships with four bundled hooks that are automatically discovered: - -- **💾 session-memory**: Saves session context to your agent workspace (default `~/.openclaw/workspace/memory/`) when you issue `/new` or `/reset` -- **📎 bootstrap-extra-files**: Injects additional workspace bootstrap files from configured glob/path patterns during `agent:bootstrap` -- **📝 command-logger**: Logs all command events to `~/.openclaw/logs/commands.log` -- **🚀 boot-md**: Runs `BOOT.md` when the gateway starts (requires internal hooks enabled) - -List available hooks: +## Quick start ```bash +# List available hooks openclaw hooks list -``` -Enable a hook: - -```bash +# Enable a hook openclaw hooks enable session-memory -``` -Check hook status: - -```bash +# Check hook status openclaw hooks check -``` -Get detailed information: - -```bash +# Get detailed information openclaw hooks info session-memory ``` -### Onboarding +## Event types -During onboarding (`openclaw onboard`), you'll be prompted to enable recommended hooks. The wizard automatically discovers eligible hooks and presents them for selection. +| Event | When it fires | +| ------------------------ | ------------------------------------------------ | +| `command:new` | `/new` command issued | +| `command:reset` | `/reset` command issued | +| `command:stop` | `/stop` command issued | +| `command` | Any command event (general listener) | +| `session:compact:before` | Before compaction summarizes history | +| `session:compact:after` | After compaction completes | +| `session:patch` | When session properties are modified | +| `agent:bootstrap` | Before workspace bootstrap files are injected | +| `gateway:startup` | After channels start and hooks are loaded | +| `message:received` | Inbound message from any channel | +| `message:transcribed` | After audio transcription completes | +| `message:preprocessed` | After all media and link understanding completes | +| `message:sent` | Outbound message delivered | -### Trust Boundary +## Writing hooks -Hooks run inside the Gateway process. Treat bundled hooks, managed hooks, and `hooks.internal.load.extraDirs` as trusted local code. Workspace hooks under `/hooks/` are repo-local code, so OpenClaw requires an explicit enable step before loading them. +### Hook structure -## Hook Discovery - -Hooks are automatically discovered from these directories, in order of increasing override precedence: - -1. **Bundled hooks**: shipped with OpenClaw; located at `/dist/hooks/bundled/` for npm installs (or a sibling `hooks/bundled/` for compiled binaries) -2. **Plugin hooks**: hooks bundled inside installed plugins (see [Plugin hooks](/plugins/architecture#provider-runtime-hooks)) -3. **Managed hooks**: `~/.openclaw/hooks/` (user-installed, shared across workspaces; can override bundled and plugin hooks). **Extra hook directories** configured via `hooks.internal.load.extraDirs` are also treated as managed hooks and share the same override precedence. -4. **Workspace hooks**: `/hooks/` (per-agent, disabled by default until explicitly enabled; cannot override hooks from other sources) - -Workspace hooks can add new hook names for a repo, but they cannot override bundled, managed, or plugin-provided hooks with the same name. - -Managed hook directories can be either a **single hook** or a **hook pack** (package directory). - -Each hook is a directory containing: +Each hook is a directory containing two files: ``` my-hook/ @@ -101,660 +63,138 @@ my-hook/ └── handler.ts # Handler implementation ``` -## Hook Packs (npm/archives) - -Hook packs are standard npm packages that export one or more hooks via `openclaw.hooks` in -`package.json`. Install them with: - -```bash -openclaw plugins install -``` - -Npm specs are registry-only (package name + optional exact version or dist-tag). -Git/URL/file specs and semver ranges are rejected. - -Bare specs and `@latest` stay on the stable track. If npm resolves either of -those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a -prerelease tag such as `@beta`/`@rc` or an exact prerelease version. - -Example `package.json`: - -```json -{ - "name": "@acme/my-hooks", - "version": "0.1.0", - "openclaw": { - "hooks": ["./hooks/my-hook", "./hooks/other-hook"] - } -} -``` - -Each entry points to a hook directory containing `HOOK.md` and a handler file. The loader tries `handler.ts`, `handler.js`, `index.ts`, `index.js` in order. -Hook packs can ship dependencies; they will be installed under `~/.openclaw/hooks/`. -Each `openclaw.hooks` entry must stay inside the package directory after symlink -resolution; entries that escape are rejected. - -Security note: `openclaw plugins install` installs hook-pack dependencies with `npm install --ignore-scripts` -(no lifecycle scripts). Keep hook pack dependency trees "pure JS/TS" and avoid packages that rely -on `postinstall` builds. - -## Hook Structure - -### HOOK.md Format - -The `HOOK.md` file contains metadata in YAML frontmatter plus Markdown documentation: +### HOOK.md format ```markdown --- name: my-hook description: "Short description of what this hook does" -homepage: https://docs.openclaw.ai/automation/hooks#my-hook metadata: { "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } } --- # My Hook -Detailed documentation goes here... - -## What It Does - -- Listens for `/new` commands -- Performs some action -- Logs the result - -## Requirements - -- Node.js must be installed - -## Configuration - -No configuration needed. +Detailed documentation goes here. ``` -### Metadata Fields +**Metadata fields** (`metadata.openclaw`): -The `metadata.openclaw` object supports: +| Field | Description | +| ---------- | ---------------------------------------------------- | +| `emoji` | Display emoji for CLI | +| `events` | Array of events to listen for | +| `export` | Named export to use (defaults to `"default"`) | +| `os` | Required platforms (e.g., `["darwin", "linux"]`) | +| `requires` | Required `bins`, `anyBins`, `env`, or `config` paths | +| `always` | Bypass eligibility checks (boolean) | +| `install` | Installation methods | -- **`emoji`**: Display emoji for CLI (e.g., `"💾"`) -- **`events`**: Array of events to listen for (e.g., `["command:new", "command:reset"]`) -- **`export`**: Named export to use (defaults to `"default"`) -- **`homepage`**: Documentation URL -- **`os`**: Required platforms (e.g., `["darwin", "linux"]`) -- **`requires`**: Optional requirements - - **`bins`**: Required binaries on PATH (e.g., `["git", "node"]`) - - **`anyBins`**: At least one of these binaries must be present - - **`env`**: Required environment variables - - **`config`**: Required config paths (e.g., `["workspace.dir"]`) -- **`always`**: Bypass eligibility checks (boolean) -- **`install`**: Installation methods (for bundled hooks: `[{"id":"bundled","kind":"bundled"}]`) - -### Handler Implementation - -The `handler.ts` file exports a `HookHandler` function: +### Handler implementation ```typescript -const myHandler = async (event) => { - // Only trigger on 'new' command +const handler = async (event) => { if (event.type !== "command" || event.action !== "new") { return; } console.log(`[my-hook] New command triggered`); - console.log(` Session: ${event.sessionKey}`); - console.log(` Timestamp: ${event.timestamp.toISOString()}`); - - // Your custom logic here + // Your logic here // Optionally send message to user - event.messages.push("✨ My hook executed!"); -}; - -export default myHandler; -``` - -#### Event Context - -Each event includes: - -```typescript -{ - type: 'command' | 'session' | 'agent' | 'gateway' | 'message', - action: string, // e.g., 'new', 'reset', 'stop', 'received', 'sent' - sessionKey: string, // Session identifier - timestamp: Date, // When the event occurred - messages: string[], // Push messages here to send to user - context: { - // Command events (command:new, command:reset): - sessionEntry?: SessionEntry, // current session entry - previousSessionEntry?: SessionEntry, // pre-reset entry (preferred for session-memory) - commandSource?: string, // e.g., 'whatsapp', 'telegram' - senderId?: string, - workspaceDir?: string, - cfg?: OpenClawConfig, - // Command events (command:stop only): - sessionId?: string, - // Agent bootstrap events (agent:bootstrap): - bootstrapFiles?: WorkspaceBootstrapFile[], - sessionKey?: string, // routing session key - sessionId?: string, // internal session UUID - agentId?: string, // resolved agent ID - // Message events (see Message Events section for full details): - from?: string, // message:received - to?: string, // message:sent - content?: string, - channelId?: string, - success?: boolean, // message:sent - } -} -``` - -## Event Types - -### Command Events - -Triggered when agent commands are issued: - -- **`command`**: All command events (general listener) -- **`command:new`**: When `/new` command is issued -- **`command:reset`**: When `/reset` command is issued -- **`command:stop`**: When `/stop` command is issued - -### Session Events - -- **`session:compact:before`**: Right before compaction summarizes history -- **`session:compact:after`**: After compaction completes with summary metadata - -Internal hook payloads emit these as `type: "session"` with `action: "compact:before"` / `action: "compact:after"`; listeners subscribe with the combined keys above. -Specific handler registration uses the literal key format `${type}:${action}`. For these events, register `session:compact:before` and `session:compact:after`. - -`session:compact:before` context fields: - -- `sessionId`: internal session UUID -- `missingSessionKey`: true when no session key was available -- `messageCount`: number of messages before compaction -- `tokenCount`: token count before compaction (may be absent) -- `messageCountOriginal`: message count from the full untruncated session history -- `tokenCountOriginal`: token count of the full original history (may be absent) - -`session:compact:after` context fields (in addition to `sessionId` and `missingSessionKey`): - -- `messageCount`: message count after compaction -- `tokenCount`: token count after compaction (may be absent) -- `compactedCount`: number of messages that were compacted/removed -- `summaryLength`: character length of the generated compaction summary -- `tokensBefore`: token count from before compaction (for delta calculation) -- `tokensAfter`: token count after compaction -- `firstKeptEntryId`: ID of the first message entry retained after compaction - -### Agent Events - -- **`agent:bootstrap`**: Before workspace bootstrap files are injected (hooks may mutate `context.bootstrapFiles`) - -### Gateway Events - -Triggered when the gateway starts: - -- **`gateway:startup`**: After channels start and hooks are loaded - -### Session Patch Events - -Triggered when session properties are modified: - -- **`session:patch`**: When a session is updated - -#### Session Event Context - -Session events include rich context about the session and changes: - -```typescript -{ - sessionEntry: SessionEntry, // The complete updated session entry - patch: { // The patch object (only changed fields) - // Session identity & labeling - label?: string | null, // Human-readable session label - - // AI model configuration - model?: string | null, // Model override (e.g., "claude-sonnet-4-6") - thinkingLevel?: string | null, // Thinking level ("off"|"low"|"med"|"high") - verboseLevel?: string | null, // Verbose output level - reasoningLevel?: string | null, // Reasoning mode override - elevatedLevel?: string | null, // Elevated mode override - responseUsage?: "off" | "tokens" | "full" | "on" | null, // Usage display mode ("on" is backwards-compat alias for "full") - fastMode?: boolean | null, // Fast/turbo mode toggle - spawnedWorkspaceDir?: string | null, // Workspace dir override for spawned subagents - subagentRole?: "orchestrator" | "leaf" | null, // Subagent role assignment - subagentControlScope?: "children" | "none" | null, // Scope of subagent control - - // Tool execution settings - execHost?: string | null, // Exec host (sandbox|gateway|node) - execSecurity?: string | null, // Security mode (deny|allowlist|full) - execAsk?: string | null, // Approval mode (off|on-miss|always) - execNode?: string | null, // Node ID for host=node - - // Subagent coordination - spawnedBy?: string | null, // Parent session key (for subagents) - spawnDepth?: number | null, // Nesting depth (0 = root) - - // Communication policies - sendPolicy?: "allow" | "deny" | null, // Message send policy - groupActivation?: "mention" | "always" | null, // Group chat activation - }, - cfg: OpenClawConfig // Current gateway config -} -``` - -**Security note:** Only privileged clients (including the Control UI) can trigger `session:patch` events. Standard WebChat clients are blocked from patching sessions, so the hook will not fire from those connections. - -See `SessionsPatchParamsSchema` in `src/gateway/protocol/schema/sessions.ts` for the complete type definition. - -#### Example: Session Patch Logger Hook - -```typescript -const handler = async (event) => { - if (event.type !== "session" || event.action !== "patch") { - return; - } - const { patch } = event.context; - console.log(`[session-patch] Session updated: ${event.sessionKey}`); - console.log(`[session-patch] Changes:`, patch); + event.messages.push("Hook executed!"); }; export default handler; ``` -### Message Events +Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (push to send to user), and `context` (event-specific data). -Triggered when messages are received or sent: +### Event context highlights -- **`message`**: All message events (general listener) -- **`message:received`**: When an inbound message is received from any channel. Fires early in processing before media understanding. Content may contain raw placeholders like `` for media attachments that haven't been processed yet. -- **`message:transcribed`**: When a message has been fully processed, including audio transcription and link understanding. At this point, `transcript` contains the full transcript text for audio messages. Use this hook when you need access to transcribed audio content. -- **`message:preprocessed`**: Fires for every message after all media + link understanding completes, giving hooks access to the fully enriched body (transcripts, image descriptions, link summaries) before the agent sees it. -- **`message:sent`**: When an outbound message is successfully sent +**Command events** (`command:new`, `command:reset`): `context.sessionEntry`, `context.previousSessionEntry`, `context.commandSource`, `context.workspaceDir`, `context.cfg`. -#### Message Event Context +**Message events** (`message:received`): `context.from`, `context.content`, `context.channelId`, `context.metadata` (provider-specific data including `senderId`, `senderName`, `guildId`). -Message events include rich context about the message: +**Message events** (`message:sent`): `context.to`, `context.content`, `context.success`, `context.channelId`. -```typescript -// message:received context -{ - from: string, // Sender identifier (phone number, user ID, etc.) - content: string, // Message content - timestamp?: number, // Unix timestamp when received - channelId: string, // Channel (e.g., "whatsapp", "telegram", "discord") - accountId?: string, // Provider account ID for multi-account setups - conversationId?: string, // Chat/conversation ID - messageId?: string, // Message ID from the provider - metadata?: { // Additional provider-specific data - to?: string, - provider?: string, - surface?: string, - threadId?: string | number, - senderId?: string, - senderName?: string, - senderUsername?: string, - senderE164?: string, - guildId?: string, // Discord guild / server ID - channelName?: string, // Channel name (e.g., Discord channel name) - } -} +**Message events** (`message:transcribed`): `context.transcript`, `context.from`, `context.channelId`, `context.mediaPath`. -// message:sent context -{ - to: string, // Recipient identifier - content: string, // Message content that was sent - success: boolean, // Whether the send succeeded - error?: string, // Error message if sending failed - channelId: string, // Channel (e.g., "whatsapp", "telegram", "discord") - accountId?: string, // Provider account ID - conversationId?: string, // Chat/conversation ID - messageId?: string, // Message ID returned by the provider - isGroup?: boolean, // Whether this outbound message belongs to a group/channel context - groupId?: string, // Group/channel identifier for correlation with message:received -} +**Message events** (`message:preprocessed`): `context.bodyForAgent` (final enriched body), `context.from`, `context.channelId`. -// message:transcribed context -{ - from?: string, // Sender identifier - to?: string, // Recipient identifier - body?: string, // Raw inbound body before enrichment - bodyForAgent?: string, // Enriched body visible to the agent - transcript: string, // Audio transcript text - timestamp?: number, // Unix timestamp when received - channelId: string, // Channel (e.g., "telegram", "whatsapp") - conversationId?: string, - messageId?: string, - senderId?: string, // Sender user ID - senderName?: string, // Sender display name - senderUsername?: string, - provider?: string, // Provider name - surface?: string, // Surface name - mediaPath?: string, // Path to the media file that was transcribed - mediaType?: string, // MIME type of the media -} +**Bootstrap events** (`agent:bootstrap`): `context.bootstrapFiles` (mutable array), `context.agentId`. -// message:preprocessed context -{ - from?: string, // Sender identifier - to?: string, // Recipient identifier - body?: string, // Raw inbound body - bodyForAgent?: string, // Final enriched body after media/link understanding - transcript?: string, // Transcript when audio was present - timestamp?: number, // Unix timestamp when received - channelId: string, // Channel (e.g., "telegram", "whatsapp") - conversationId?: string, - messageId?: string, - senderId?: string, // Sender user ID - senderName?: string, // Sender display name - senderUsername?: string, - provider?: string, // Provider name - surface?: string, // Surface name - mediaPath?: string, // Path to the media file - mediaType?: string, // MIME type of the media - isGroup?: boolean, - groupId?: string, -} +**Session patch events** (`session:patch`): `context.sessionEntry`, `context.patch` (only changed fields), `context.cfg`. Only privileged clients can trigger patch events. + +**Compaction events**: `session:compact:before` includes `messageCount`, `tokenCount`. `session:compact:after` adds `compactedCount`, `summaryLength`, `tokensBefore`, `tokensAfter`. + +## Hook discovery + +Hooks are discovered from these directories, in order of increasing override precedence: + +1. **Bundled hooks**: shipped with OpenClaw +2. **Plugin hooks**: hooks bundled inside installed plugins +3. **Managed hooks**: `~/.openclaw/hooks/` (user-installed, shared across workspaces). Extra directories from `hooks.internal.load.extraDirs` share this precedence. +4. **Workspace hooks**: `/hooks/` (per-agent, disabled by default until explicitly enabled) + +Workspace hooks can add new hook names but cannot override bundled, managed, or plugin-provided hooks with the same name. + +### Hook packs + +Hook packs are npm packages that export hooks via `openclaw.hooks` in `package.json`. Install with: + +```bash +openclaw plugins install ``` -#### Example: Message Logger Hook +Npm specs are registry-only (package name + optional exact version or dist-tag). Git/URL/file specs and semver ranges are rejected. -```typescript -const isMessageReceivedEvent = (event: { type: string; action: string }) => - event.type === "message" && event.action === "received"; -const isMessageSentEvent = (event: { type: string; action: string }) => - event.type === "message" && event.action === "sent"; +## Bundled hooks -const handler = async (event) => { - if (isMessageReceivedEvent(event as { type: string; action: string })) { - console.log(`[message-logger] Received from ${event.context.from}: ${event.context.content}`); - } else if (isMessageSentEvent(event as { type: string; action: string })) { - console.log(`[message-logger] Sent to ${event.context.to}: ${event.context.content}`); - } -}; +| Hook | Events | What it does | +| --------------------- | ------------------------------ | ----------------------------------------------------- | +| session-memory | `command:new`, `command:reset` | Saves session context to `/memory/` | +| bootstrap-extra-files | `agent:bootstrap` | Injects additional bootstrap files from glob patterns | +| command-logger | `command` | Logs all commands to `~/.openclaw/logs/commands.log` | +| boot-md | `gateway:startup` | Runs `BOOT.md` when the gateway starts | -export default handler; +Enable any bundled hook: + +```bash +openclaw hooks enable ``` -### Tool Result Hooks (Plugin API) +### session-memory details -These hooks are not event-stream listeners; they let plugins synchronously adjust tool results before OpenClaw persists them. +Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `/memory/YYYY-MM-DD-slug.md`. Requires `workspace.dir` to be configured. -- **`tool_result_persist`**: transform tool results before they are written to the session transcript. Must be synchronous; return the updated tool result payload or `undefined` to keep it as-is. See [Agent Loop](/concepts/agent-loop). - -### Plugin Hook Events - -#### before_tool_call - -Runs before each tool call. Plugins can modify parameters, block the call, or request user approval. - -Return fields: - -- **`params`**: Override tool parameters (merged with original params) -- **`block`**: Set to `true` to block the tool call -- **`blockReason`**: Reason shown to the agent when blocked -- **`requireApproval`**: Pause execution and wait for user approval via channels - -The `requireApproval` field triggers native platform approval (Telegram buttons, Discord components, `/approve` command) instead of relying on the agent to cooperate: - -```typescript -{ - requireApproval: { - title: "Sensitive operation", - description: "This tool call modifies production data", - severity: "warning", // "info" | "warning" | "critical" - timeoutMs: 120000, // default: 120s - timeoutBehavior: "deny", // "allow" | "deny" (default) - onResolution: async (decision) => { - // Called after the user resolves: "allow-once", "allow-always", "deny", "timeout", or "cancelled" - }, - } -} -``` - -The `onResolution` callback is invoked with the final decision string after the approval resolves, times out, or is cancelled. It runs in-process within the plugin (not sent to the gateway). Use it to persist decisions, update caches, or perform cleanup. - -The `pluginId` field is stamped automatically by the hook runner from the plugin registration. When multiple plugins return `requireApproval`, the first one (highest priority) wins. - -`block` takes precedence over `requireApproval`: if the merged hook result has both `block: true` and a `requireApproval` field, the tool call is blocked immediately without triggering the approval flow. This ensures a higher-priority plugin's block cannot be overridden by a lower-priority plugin's approval request. - -If the gateway is unavailable or does not support plugin approvals, the tool call falls back to a soft block using the `description` as the block reason. - -#### before_install - -Runs after the built-in install security scan and before installation continues. OpenClaw fires this hook for interactive skill installs as well as plugin bundle, package, and single-file installs. - -Default behavior differs by target type: - -- Plugin installs fail closed on built-in scan `critical` findings and scan errors unless the operator explicitly uses `openclaw plugins install --dangerously-force-unsafe-install`. -- Skill installs still surface built-in scan findings and scan errors as warnings and continue by default. - -Return fields: - -- **`findings`**: Additional scan findings to surface as warnings -- **`block`**: Set to `true` to block the install -- **`blockReason`**: Human-readable reason shown when blocked - -Event fields: - -- **`targetType`**: Install target category (`skill` or `plugin`) -- **`targetName`**: Human-readable skill name or plugin id for the install target -- **`sourcePath`**: Absolute path to the install target content being scanned -- **`sourcePathKind`**: Whether the scanned content is a `file` or `directory` -- **`origin`**: Normalized install origin when available (for example `openclaw-bundled`, `openclaw-workspace`, `plugin-bundle`, `plugin-package`, or `plugin-file`) -- **`request`**: Provenance for the install request, including `kind`, `mode`, and optional `requestedSpecifier` -- **`builtinScan`**: Structured result of the built-in scanner, including `status`, summary counts, findings, and optional `error` -- **`skill`**: Skill install metadata when `targetType` is `skill`, including `installId` and the selected `installSpec` -- **`plugin`**: Plugin install metadata when `targetType` is `plugin`, including the canonical `pluginId`, normalized `contentType`, optional `packageName` / `manifestId` / `version`, and `extensions` - -Example event (plugin package install): +### bootstrap-extra-files config ```json { - "targetType": "plugin", - "targetName": "acme-audit", - "sourcePath": "/var/folders/.../openclaw-plugin-acme-audit/package", - "sourcePathKind": "directory", - "origin": "plugin-package", - "request": { - "kind": "plugin-npm", - "mode": "install", - "requestedSpecifier": "@acme/openclaw-plugin-audit@1.4.2" - }, - "builtinScan": { - "status": "ok", - "scannedFiles": 12, - "critical": 0, - "warn": 1, - "info": 0, - "findings": [ - { - "severity": "warn", - "ruleId": "network_fetch", - "file": "dist/index.js", - "line": 88, - "message": "Dynamic network fetch detected during install review." + "hooks": { + "internal": { + "entries": { + "bootstrap-extra-files": { + "enabled": true, + "paths": ["packages/*/AGENTS.md", "packages/*/TOOLS.md"] + } } - ] - }, - "plugin": { - "pluginId": "acme-audit", - "contentType": "package", - "packageName": "@acme/openclaw-plugin-audit", - "manifestId": "acme-audit", - "version": "1.4.2", - "extensions": ["./dist/index.js"] + } } } ``` -Skill installs use the same event shape with `targetType: "skill"` and a `skill` object instead of `plugin`. +Paths resolve relative to workspace. Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`). -Decision semantics: +## Plugin hooks -- `before_install`: `{ block: true }` is terminal and stops lower-priority handlers. -- `before_install`: `{ block: false }` is treated as no decision. +Plugins can register hooks through the Plugin SDK for deeper integration: intercepting tool calls, modifying prompts, controlling message flow, and more. The Plugin SDK exposes 28 hooks covering model resolution, agent lifecycle, message flow, tool execution, subagent coordination, and gateway lifecycle. -Use this hook for external security scanners, policy engines, or enterprise approval gates that need to audit install sources before they are installed. - -#### Compaction lifecycle - -Compaction lifecycle hooks exposed through the plugin hook runner: - -- **`before_compaction`**: Runs before compaction with count/token metadata -- **`after_compaction`**: Runs after compaction with compaction summary metadata - -### Complete Plugin Hook Reference - -All 28 hooks registered via the Plugin SDK. Hooks marked **sequential** run in priority order and can modify results; **parallel** hooks are fire-and-forget. - -#### Model and prompt hooks - -| Hook | When | Execution | Returns | -| ---------------------- | -------------------------------------------- | ---------- | ---------------------------------------------------------- | -| `before_model_resolve` | Before model/provider lookup | Sequential | `{ modelOverride?, providerOverride? }` | -| `before_prompt_build` | After model resolved, session messages ready | Sequential | `{ systemPrompt?, prependContext?, appendSystemContext? }` | -| `before_agent_start` | Legacy combined hook (prefer the two above) | Sequential | Union of both result shapes | -| `before_agent_reply` | After inline actions, before the LLM runs | Sequential | `{ handled: boolean, reply?, reason? }` | -| `llm_input` | Immediately before the LLM API call | Parallel | `void` | -| `llm_output` | Immediately after LLM response received | Parallel | `void` | - -#### Agent lifecycle hooks - -| Hook | When | Execution | Returns | -| ------------------- | ---------------------------------------------- | --------- | ------- | -| `agent_end` | After agent run completes (success or failure) | Parallel | `void` | -| `before_reset` | When `/new` or `/reset` clears a session | Parallel | `void` | -| `before_compaction` | Before compaction summarizes history | Parallel | `void` | -| `after_compaction` | After compaction completes | Parallel | `void` | - -#### Session lifecycle hooks - -| Hook | When | Execution | Returns | -| --------------- | ------------------------- | --------- | ------- | -| `session_start` | When a new session begins | Parallel | `void` | -| `session_end` | When a session ends | Parallel | `void` | - -#### Message flow hooks - -| Hook | When | Execution | Returns | -| ---------------------- | ------------------------------------------------- | -------------------- | ----------------------------- | -| `inbound_claim` | Before command/agent dispatch; first-claim wins | Sequential | `{ handled: boolean }` | -| `message_received` | After an inbound message is received | Parallel | `void` | -| `before_dispatch` | After commands parsed, before model dispatch | Sequential | `{ handled: boolean, text? }` | -| `message_sending` | Before an outbound message is delivered | Sequential | `{ content?, cancel? }` | -| `message_sent` | After an outbound message is delivered | Parallel | `void` | -| `before_message_write` | Before a message is written to session transcript | **Sync**, sequential | `{ block?, message? }` | - -#### Tool execution hooks - -| Hook | When | Execution | Returns | -| --------------------- | --------------------------------------------- | -------------------- | ----------------------------------------------------- | -| `before_tool_call` | Before each tool call | Sequential | `{ params?, block?, blockReason?, requireApproval? }` | -| `after_tool_call` | After a tool call completes | Parallel | `void` | -| `tool_result_persist` | Before a tool result is written to transcript | **Sync**, sequential | `{ message? }` | - -#### Subagent hooks - -| Hook | When | Execution | Returns | -| -------------------------- | ------------------------------------------ | ---------- | --------------------------------- | -| `subagent_spawning` | Before a subagent session is created | Sequential | `{ status, threadBindingReady? }` | -| `subagent_delivery_target` | After spawning, to resolve delivery target | Sequential | `{ origin? }` | -| `subagent_spawned` | After a subagent is fully spawned | Parallel | `void` | -| `subagent_ended` | When a subagent session terminates | Parallel | `void` | - -#### Gateway hooks - -| Hook | When | Execution | Returns | -| --------------- | ------------------------------------------ | --------- | ------- | -| `gateway_start` | After the gateway process is fully started | Parallel | `void` | -| `gateway_stop` | When the gateway is shutting down | Parallel | `void` | - -#### Install hooks - -| Hook | When | Execution | Returns | -| ---------------- | ----------------------------------------------------- | ---------- | ------------------------------------- | -| `before_install` | After built-in security scan, before install proceeds | Sequential | `{ findings?, block?, blockReason? }` | - - -Two hooks (`tool_result_persist` and `before_message_write`) are **synchronous only** — they must not return a Promise. Returning a Promise from these hooks is caught at runtime and the result is discarded with a warning. - - -For full handler signatures and context types, see [Plugin Architecture](/plugins/architecture). - -### Future Events - -The following event types are planned for the internal hook event stream. -Note that `session_start` and `session_end` already exist as [Plugin Hook API](/plugins/architecture#provider-runtime-hooks) hooks -but are not yet available as internal hook event keys in `HOOK.md` metadata: - -- **`session:start`**: When a new session begins (planned for internal hook stream; available as plugin hook `session_start`) -- **`session:end`**: When a session ends (planned for internal hook stream; available as plugin hook `session_end`) -- **`agent:error`**: When an agent encounters an error - -## Creating Custom Hooks - -### 1. Choose Location - -- **Workspace hooks** (`/hooks/`): Per-agent; can add new hook names but cannot override bundled, managed, or plugin hooks with the same name -- **Managed hooks** (`~/.openclaw/hooks/`): Shared across workspaces; can override bundled and plugin hooks - -### 2. Create Directory Structure - -```bash -mkdir -p ~/.openclaw/hooks/my-hook -cd ~/.openclaw/hooks/my-hook -``` - -### 3. Create HOOK.md - -```markdown ---- -name: my-hook -description: "Does something useful" -metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } } ---- - -# My Custom Hook - -This hook does something useful when you issue `/new`. -``` - -### 4. Create handler.ts - -```typescript -const handler = async (event) => { - if (event.type !== "command" || event.action !== "new") { - return; - } - - console.log("[my-hook] Running!"); - // Your logic here -}; - -export default handler; -``` - -### 5. Enable and Test - -```bash -# Verify hook is discovered -openclaw hooks list - -# Enable it -openclaw hooks enable my-hook - -# Restart your gateway process (menu bar app restart on macOS, or restart your dev process) - -# Trigger the event -# Send /new via your messaging channel -``` +For the complete plugin hook reference including `before_tool_call`, `before_agent_reply`, `before_install`, and all other plugin hooks, see [Plugin Architecture](/plugins/architecture#provider-runtime-hooks). ## Configuration -### New Config Format (Recommended) - ```json { "hooks": { @@ -769,21 +209,16 @@ openclaw hooks enable my-hook } ``` -### Per-Hook Configuration - -Hooks can have custom configuration: +Per-hook environment variables: ```json { "hooks": { "internal": { - "enabled": true, "entries": { "my-hook": { "enabled": true, - "env": { - "MY_CUSTOM_VAR": "value" - } + "env": { "MY_CUSTOM_VAR": "value" } } } } @@ -791,15 +226,12 @@ Hooks can have custom configuration: } ``` -### Extra Directories - -Load hooks from additional directories (treated as managed hooks, same override precedence): +Extra hook directories: ```json { "hooks": { "internal": { - "enabled": true, "load": { "extraDirs": ["/path/to/more/hooks"] } @@ -808,569 +240,64 @@ Load hooks from additional directories (treated as managed hooks, same override } ``` -### Legacy Config Format (Still Supported) + +The legacy `hooks.internal.handlers` array config format is still supported for backwards compatibility, but new hooks should use the discovery-based system. + -The old config format still works for backwards compatibility: - -```json -{ - "hooks": { - "internal": { - "enabled": true, - "handlers": [ - { - "event": "command:new", - "module": "./hooks/handlers/my-handler.ts", - "export": "default" - } - ] - } - } -} -``` - -Note: `module` must be a workspace-relative path. Absolute paths and traversal outside the workspace are rejected. - -**Migration**: Use the new discovery-based system for new hooks. Legacy handlers are loaded after directory-based hooks. - -## CLI Commands - -### List Hooks +## CLI reference ```bash -# List all hooks +# List all hooks (add --eligible, --verbose, or --json) openclaw hooks list -# Show only eligible hooks -openclaw hooks list --eligible - -# Verbose output (show missing requirements) -openclaw hooks list --verbose - -# JSON output -openclaw hooks list --json -``` - -### Hook Information - -```bash # Show detailed info about a hook -openclaw hooks info session-memory +openclaw hooks info -# JSON output -openclaw hooks info session-memory --json -``` - -### Check Eligibility - -```bash # Show eligibility summary openclaw hooks check -# JSON output -openclaw hooks check --json +# Enable/disable +openclaw hooks enable +openclaw hooks disable ``` -### Enable/Disable +## Best practices -```bash -# Enable a hook -openclaw hooks enable session-memory - -# Disable a hook -openclaw hooks disable command-logger -``` - -## Bundled hook reference - -### session-memory - -Saves session context to memory when you issue `/new` or `/reset`. - -**Events**: `command:new`, `command:reset` - -**Requirements**: `workspace.dir` must be configured - -**Output**: `/memory/YYYY-MM-DD-slug.md` (defaults to `~/.openclaw/workspace`) - -**What it does**: - -1. Uses the pre-reset session entry to locate the correct transcript -2. Extracts the last 15 user/assistant messages from the conversation (configurable) -3. Uses LLM to generate a descriptive filename slug -4. Saves session metadata to a dated memory file - -**Example output**: - -```markdown -# Session: 2026-01-16 14:30:00 UTC - -- **Session Key**: agent:main:main -- **Session ID**: abc123def456 -- **Source**: telegram - -## Conversation Summary - -user: Can you help me design the API? -assistant: Sure! Let's start with the endpoints... -``` - -**Filename examples**: - -- `2026-01-16-vendor-pitch.md` -- `2026-01-16-api-design.md` -- `2026-01-16-1430.md` (fallback timestamp if slug generation fails) - -**Enable**: - -```bash -openclaw hooks enable session-memory -``` - -### bootstrap-extra-files - -Injects additional bootstrap files (for example monorepo-local `AGENTS.md` / `TOOLS.md`) during `agent:bootstrap`. - -**Events**: `agent:bootstrap` - -**Requirements**: `workspace.dir` must be configured - -**Output**: No files written; bootstrap context is modified in-memory only. - -**Config**: - -```json -{ - "hooks": { - "internal": { - "enabled": true, - "entries": { - "bootstrap-extra-files": { - "enabled": true, - "paths": ["packages/*/AGENTS.md", "packages/*/TOOLS.md"] - } - } - } - } -} -``` - -**Config options**: - -- `paths` (string[]): glob/path patterns to resolve from the workspace. -- `patterns` (string[]): alias of `paths`. -- `files` (string[]): alias of `paths`. - -**Notes**: - -- Paths are resolved relative to workspace. -- Files must stay inside workspace (realpath-checked). -- Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`, `memory.md`). -- For subagent/cron sessions a narrower allowlist applies (`AGENTS.md`, `TOOLS.md`, `SOUL.md`, `IDENTITY.md`, `USER.md`). - -**Enable**: - -```bash -openclaw hooks enable bootstrap-extra-files -``` - -### command-logger - -Logs all command events to a centralized audit file. - -**Events**: `command` - -**Requirements**: None - -**Output**: `~/.openclaw/logs/commands.log` - -**What it does**: - -1. Captures event details (command action, timestamp, session key, sender ID, source) -2. Appends to log file in JSONL format -3. Runs silently in the background - -**Example log entries**: - -```jsonl -{"timestamp":"2026-01-16T14:30:00.000Z","action":"new","sessionKey":"agent:main:main","senderId":"+1234567890","source":"telegram"} -{"timestamp":"2026-01-16T15:45:22.000Z","action":"stop","sessionKey":"agent:main:main","senderId":"user@example.com","source":"whatsapp"} -``` - -**View logs**: - -```bash -# View recent commands -tail -n 20 ~/.openclaw/logs/commands.log - -# Pretty-print with jq -cat ~/.openclaw/logs/commands.log | jq . - -# Filter by action -grep '"action":"new"' ~/.openclaw/logs/commands.log | jq . -``` - -**Enable**: - -```bash -openclaw hooks enable command-logger -``` - -### boot-md - -Runs `BOOT.md` when the gateway starts (after channels start). -Internal hooks must be enabled for this to run. - -**Events**: `gateway:startup` - -**Requirements**: `workspace.dir` must be configured - -**What it does**: - -1. Reads `BOOT.md` from your workspace -2. Runs the instructions via the agent runner -3. Sends any requested outbound messages via the message tool - -**Enable**: - -```bash -openclaw hooks enable boot-md -``` - -## Best Practices - -### Keep Handlers Fast - -Hooks run during command processing. Keep them lightweight: - -```typescript -// ✓ Good - async work, returns immediately -const handler: HookHandler = async (event) => { - void processInBackground(event); // Fire and forget -}; - -// ✗ Bad - blocks command processing -const handler: HookHandler = async (event) => { - await slowDatabaseQuery(event); - await evenSlowerAPICall(event); -}; -``` - -### Handle Errors Gracefully - -Always wrap risky operations: - -```typescript -const handler: HookHandler = async (event) => { - try { - await riskyOperation(event); - } catch (err) { - console.error("[my-handler] Failed:", err instanceof Error ? err.message : String(err)); - // Don't throw - let other handlers run - } -}; -``` - -### Filter Events Early - -Return early if the event isn't relevant: - -```typescript -const handler: HookHandler = async (event) => { - // Only handle 'new' commands - if (event.type !== "command" || event.action !== "new") { - return; - } - - // Your logic here -}; -``` - -### Use Specific Event Keys - -Specify exact events in metadata when possible: - -```yaml -metadata: { "openclaw": { "events": ["command:new"] } } # Specific -``` - -Rather than: - -```yaml -metadata: { "openclaw": { "events": ["command"] } } # General - more overhead -``` - -## Debugging - -### Enable Hook Logging - -The gateway logs hook loading at startup: - -```text -Registered hook: session-memory -> command:new, command:reset -Registered hook: bootstrap-extra-files -> agent:bootstrap -Registered hook: command-logger -> command -Registered hook: boot-md -> gateway:startup -``` - -### Check Discovery - -List all discovered hooks: - -```bash -openclaw hooks list --verbose -``` - -### Check Registration - -In your handler, log when it's called: - -```typescript -const handler: HookHandler = async (event) => { - console.log("[my-handler] Triggered:", event.type, event.action); - // Your logic -}; -``` - -### Verify Eligibility - -Check why a hook isn't eligible: - -```bash -openclaw hooks info my-hook -``` - -Look for missing requirements in the output. - -## Testing - -### Gateway Logs - -Monitor gateway logs to see hook execution: - -```bash -# macOS -./scripts/clawlog.sh -f - -# Other platforms -tail -f ~/.openclaw/gateway.log -``` - -### Test Hooks Directly - -Test your handlers in isolation: - -```typescript -import { test } from "vitest"; -import myHandler from "./hooks/my-hook/handler.js"; - -test("my handler works", async () => { - const event = { - type: "command", - action: "new", - sessionKey: "test-session", - timestamp: new Date(), - messages: [], - context: { foo: "bar" }, - }; - - await myHandler(event); - - // Assert side effects -}); -``` - -## Architecture - -### Core Components - -- **`src/hooks/types.ts`**: Type definitions -- **`src/hooks/workspace.ts`**: Directory scanning and loading -- **`src/hooks/frontmatter.ts`**: HOOK.md metadata parsing -- **`src/hooks/config.ts`**: Eligibility checking -- **`src/hooks/hooks-status.ts`**: Status reporting -- **`src/hooks/loader.ts`**: Dynamic module loader -- **`src/cli/hooks-cli.ts`**: CLI commands -- **`src/gateway/server-startup.ts`**: Loads hooks at gateway start -- **`src/auto-reply/reply/commands-core.ts`**: Triggers command events - -### Discovery Flow - -``` -Gateway startup - ↓ -Scan directories (bundled → plugin → managed + extra dirs → workspace) - ↓ -Parse HOOK.md files - ↓ -Sort by override precedence (bundled < plugin < managed < workspace) - ↓ -Check eligibility (bins, env, config, os) - ↓ -Load handlers from eligible hooks - ↓ -Register handlers for events -``` - -### Event Flow - -``` -User sends /new - ↓ -Command validation - ↓ -Create hook event - ↓ -Trigger hook (all registered handlers) - ↓ -Command processing continues - ↓ -Session reset -``` +- **Keep handlers fast.** Hooks run during command processing. Fire-and-forget heavy work with `void processInBackground(event)`. +- **Handle errors gracefully.** Wrap risky operations in try/catch; do not throw so other handlers can run. +- **Filter events early.** Return immediately if the event type/action is not relevant. +- **Use specific event keys.** Prefer `"events": ["command:new"]` over `"events": ["command"]` to reduce overhead. ## Troubleshooting -### Hook Not Discovered +### Hook not discovered -1. Check directory structure: +```bash +# Verify directory structure +ls -la ~/.openclaw/hooks/my-hook/ +# Should show: HOOK.md, handler.ts - ```bash - ls -la ~/.openclaw/hooks/my-hook/ - # Should show: HOOK.md, handler.ts - ``` +# List all discovered hooks +openclaw hooks list +``` -2. Verify HOOK.md format: - - ```bash - cat ~/.openclaw/hooks/my-hook/HOOK.md - # Should have YAML frontmatter with name and metadata - ``` - -3. List all discovered hooks: - - ```bash - openclaw hooks list - ``` - -### Hook Not Eligible - -Check requirements: +### Hook not eligible ```bash openclaw hooks info my-hook ``` -Look for missing: +Check for missing binaries (PATH), environment variables, config values, or OS compatibility. -- Binaries (check PATH) -- Environment variables -- Config values -- OS compatibility - -### Hook Not Executing - -1. Verify hook is enabled: - - ```bash - openclaw hooks list - # Should show ✓ next to enabled hooks - ``` +### Hook not executing +1. Verify the hook is enabled: `openclaw hooks list` 2. Restart your gateway process so hooks reload. +3. Check gateway logs: `./scripts/clawlog.sh | grep hook` -3. Check gateway logs for errors: - - ```bash - ./scripts/clawlog.sh | grep hook - ``` - -### Handler Errors - -Check for TypeScript/import errors: - -```bash -# Test import directly -node -e "import('./path/to/handler.ts').then(console.log)" -``` - -## Migration Guide - -### From Legacy Config to Discovery - -**Before**: - -```json -{ - "hooks": { - "internal": { - "enabled": true, - "handlers": [ - { - "event": "command:new", - "module": "./hooks/handlers/my-handler.ts" - } - ] - } - } -} -``` - -**After**: - -1. Create hook directory: - - ```bash - mkdir -p ~/.openclaw/hooks/my-hook - mv ./hooks/handlers/my-handler.ts ~/.openclaw/hooks/my-hook/handler.ts - ``` - -2. Create HOOK.md: - - ```markdown - --- - name: my-hook - description: "My custom hook" - metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } } - --- - - # My Hook - - Does something useful. - ``` - -3. Update config: - - ```json - { - "hooks": { - "internal": { - "enabled": true, - "entries": { - "my-hook": { "enabled": true } - } - } - } - } - ``` - -4. Verify and restart your gateway process: - - ```bash - openclaw hooks list - # Should show: 🎯 my-hook ✓ - ``` - -**Benefits of migration**: - -- Automatic discovery -- CLI management -- Eligibility checking -- Better documentation -- Consistent structure - -## See Also +## Related - [CLI Reference: hooks](/cli/hooks) -- [Bundled Hooks README](https://github.com/openclaw/openclaw/tree/main/src/hooks/bundled) - [Webhooks](/automation/cron-jobs#webhooks) +- [Plugin Architecture](/plugins/architecture#provider-runtime-hooks) — full plugin hook reference - [Configuration](/gateway/configuration-reference#hooks) diff --git a/docs/automation/index.md b/docs/automation/index.md index 0a7514d70f..c68a6b52b4 100644 --- a/docs/automation/index.md +++ b/docs/automation/index.md @@ -33,48 +33,60 @@ flowchart TD | React to commands or lifecycle | Hooks | Event-driven, runs custom scripts | | Persistent agent instructions | Standing orders | Injected into every session | +### Cron vs heartbeat + +| Dimension | Cron | Heartbeat | +| --------------- | ----------------------------------- | ------------------------------------- | +| Timing | Exact (cron expressions, one-shot) | Approximate (default every 30 min) | +| Session context | Fresh (isolated) or shared | Full main-session context | +| Task records | Always created | Never created | +| Delivery | Channel, webhook, or silent | Inline in main session | +| Best for | Reports, reminders, background jobs | Inbox checks, calendar, notifications | + +Use cron when you need precise timing or isolated execution. Use heartbeat when the work benefits from full session context and approximate timing is fine. + ## Core concepts +### Scheduled tasks (cron) + +Cron is the Gateway's built-in scheduler for precise timing. It persists jobs, wakes the agent at the right time, and can deliver output to a chat channel or webhook endpoint. Supports one-shot reminders, recurring expressions, and inbound webhook triggers. + +See [Scheduled Tasks](/automation/cron-jobs). + ### Tasks The background task ledger tracks all detached work: ACP runs, subagent spawns, isolated cron executions, and CLI operations. Tasks are records, not schedulers. Use `openclaw tasks list` and `openclaw tasks audit` to inspect them. See [Background Tasks](/automation/tasks). -### Scheduled tasks (cron) - -Cron is the Gateway's built-in scheduler for precise timing. It persists jobs, wakes the agent at the right time, and can deliver output to a chat channel or webhook. Supports one-shot reminders, recurring expressions, and inbound webhook triggers. - -See [Scheduled Tasks](/automation/cron-jobs). - ### Task Flow Task Flow is the flow orchestration substrate above background tasks. It manages durable multi-step flows with managed and mirrored sync modes, revision tracking, and `openclaw tasks flow list|show|cancel` for inspection. See [Task Flow](/automation/taskflow). -### Heartbeat - -Heartbeat is a periodic main-session turn (default every 30 minutes). It batches multiple checks (inbox, calendar, notifications) in one agent turn with full session context. Heartbeat turns do not create task records. Use `HEARTBEAT.md` to define what the agent checks. - -See [Heartbeat](/gateway/heartbeat). - -### Hooks - -Hooks are event-driven scripts triggered by agent lifecycle events (`/new`, `/reset`, `/stop`), session compaction, gateway startup, message flow, and tool calls. Hooks are automatically discovered from directories and can be managed with `openclaw hooks`. - -See [Hooks](/automation/hooks). - ### Standing orders Standing orders grant the agent permanent operating authority for defined programs. They live in workspace files (typically `AGENTS.md`) and are injected into every session. Combine with cron for time-based enforcement. See [Standing Orders](/automation/standing-orders). +### Hooks + +Hooks are event-driven scripts triggered by agent lifecycle events (`/new`, `/reset`, `/stop`), session compaction, gateway startup, message flow, and tool calls. Hooks are automatically discovered from directories and can be managed with `openclaw hooks`. + +See [Hooks](/automation/hooks). + +### Heartbeat + +Heartbeat is a periodic main-session turn (default every 30 minutes). It batches multiple checks (inbox, calendar, notifications) in one agent turn with full session context. Heartbeat turns do not create task records. Use `HEARTBEAT.md` to define what the agent checks. + +See [Heartbeat](/gateway/heartbeat). + ## How they work together -- **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes. - **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders. All cron executions create task records. +- **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes. - **Hooks** react to specific events (tool calls, session resets, compaction) with custom scripts. - **Standing orders** give the agent persistent context and authority boundaries. - **Task Flow** coordinates multi-step flows above individual tasks. diff --git a/docs/automation/taskflow.md b/docs/automation/taskflow.md index 4db6f44f49..8597666cf0 100644 --- a/docs/automation/taskflow.md +++ b/docs/automation/taskflow.md @@ -11,16 +11,45 @@ title: "Task Flow" Task Flow is the flow orchestration substrate that sits above [background tasks](/automation/tasks). It manages durable multi-step flows with their own state, revision tracking, and sync semantics while individual tasks remain the unit of detached work. +## When to use Task Flow + +Use Task Flow when work spans multiple sequential or branching steps and you need durable progress tracking across gateway restarts. For single background operations, a plain [task](/automation/tasks) is sufficient. + +| Scenario | Use | +| ------------------------------------- | -------------------- | +| Single background job | Plain task | +| Multi-step pipeline (A then B then C) | Task Flow (managed) | +| Observe externally created tasks | Task Flow (mirrored) | +| One-shot reminder | Cron job | + ## Sync modes -Task Flow supports two sync modes: +### Managed mode -- **Managed** — Task Flow owns the lifecycle end-to-end, creating and driving tasks as flow steps progress. -- **Mirrored** — Task Flow observes externally created tasks and keeps flow state in sync without taking ownership of task creation. +Task Flow owns the lifecycle end-to-end. It creates tasks as flow steps, drives them to completion, and advances the flow state automatically. + +Example: a weekly report flow that (1) gathers data, (2) generates the report, and (3) delivers it. Task Flow creates each step as a background task, waits for completion, then moves to the next step. + +``` +Flow: weekly-report + Step 1: gather-data → task created → succeeded + Step 2: generate-report → task created → succeeded + Step 3: deliver → task created → running +``` + +### Mirrored mode + +Task Flow observes externally created tasks and keeps flow state in sync without taking ownership of task creation. This is useful when tasks originate from cron jobs, CLI commands, or other sources and you want a unified view of their progress as a flow. + +Example: three independent cron jobs that together form a "morning ops" routine. A mirrored flow tracks their collective progress without controlling when or how they run. ## Durable state and revision tracking -Each flow persists its own state and tracks revisions so progress survives gateway restarts. Revision tracking enables conflict detection when multiple sources attempt to advance the same flow. +Each flow persists its own state and tracks revisions so progress survives gateway restarts. Revision tracking enables conflict detection when multiple sources attempt to advance the same flow concurrently. + +## Cancel behavior + +`openclaw tasks flow cancel` sets a sticky cancel intent on the flow. Active tasks within the flow are cancelled, and no new steps are started. The cancel intent persists across restarts, so a cancelled flow stays cancelled even if the gateway restarts before all child tasks have terminated. ## CLI commands @@ -31,13 +60,15 @@ openclaw tasks flow list # Show details for a specific flow openclaw tasks flow show -# Cancel a running flow +# Cancel a running flow and its active tasks openclaw tasks flow cancel ``` -- `openclaw tasks flow list` — shows tracked flows with status and sync mode -- `openclaw tasks flow show ` — inspect one flow by flow id or lookup key -- `openclaw tasks flow cancel ` — cancel a running flow and its active tasks +| Command | Description | +| --------------------------------- | --------------------------------------------- | +| `openclaw tasks flow list` | Shows tracked flows with status and sync mode | +| `openclaw tasks flow show ` | Inspect one flow by flow id or lookup key | +| `openclaw tasks flow cancel ` | Cancel a running flow and its active tasks | ## How flows relate to tasks diff --git a/docs/docs.json b/docs/docs.json index 662dc2b4f4..1f6b3a82b0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1145,11 +1145,11 @@ "group": "Automation & Tasks", "pages": [ "automation/index", - "automation/tasks", "automation/cron-jobs", + "automation/tasks", "automation/taskflow", - "automation/hooks", - "automation/standing-orders" + "automation/standing-orders", + "automation/hooks" ] }, {