AI Harness Engineering · OpenClaw 11 of 11 · Anatomy of an Agent Gateway

Skills, Plugins, Hooks & Automation — Extending the Agent

A gateway you can't extend is a toy. OpenClaw grows in four ways — skills it loads on demand, plugins that add whole subsystems, hooks that fire on events, and cron jobs that run on a schedule. This finale builds all four extension surfaces and ties the entire series together.

Prerequisites: Lessons 1–10 (the whole series). You know roughly what a cron expression looks like. A little Python for the labs.
9
Chapters
2
Simulations
3
Code Labs

Chapter 0: Four Ways to Extend a Gateway

The whole lesson in one sentence. A gateway that can only do what it shipped with is a dead end — so OpenClaw exposes four ways to grow it without editing the core: skills (knowledge and procedures loaded on demand), plugins (whole subsystems bolted on), hooks (your code firing on gateway events), and cron (tasks that run on a schedule). This finale builds all four, then steps back to see the entire gateway you've assembled across eleven lessons.

By Lesson 10 your gateway is complete in the sense that matters: it owns the connections, speaks a protocol, authorizes clients, runs an agent loop with memory and tools, and survives its own crashes. But it is fixed. Everything it knows and everything it can do was baked in when you built it. The instant you want it to learn a new procedure, talk to a new chat app, react to a new event, or do something at 8 AM whether or not you're awake — you've hit the wall.

The naive way past that wall is to keep stuffing the core. Want the agent to know your company's deploy runbook? Paste it into the system prompt. Want a Matrix channel? Add a branch to the surface code. Want a daily digest? Add a setInterval somewhere. Do this for a year and the prompt is a bloated wall of text the model mostly ignores, and the core is a thicket of special cases nobody dares touch.

The core tension: capability vs. weight. Every ability you want the agent to have is a temptation to bake it in — into the prompt, into the core. But the prompt has a finite budget the model actually reads, and the core has a finite complexity a human can maintain. So the real design question is never "how do we add this ability?" It is "how do we add it without permanently weighing down the prompt or the core?" Each of the four surfaces is a different answer to exactly that.

Here is the shape of the four answers, each loading its cost only when needed:

Skills
knowledge & procedures, loaded on demand — read only when relevant
·
Plugins
whole subsystems — channels, tools, memory backends, pipelines
·
Hooks
your code, fired on gateway/agent events
·
Cron
tasks on a schedule — proactive, no human in the loop

Notice the through-line: on demand. A skill costs the prompt nothing until the model decides to read it. A plugin sits dormant until its event fires. A hook is silent until its trigger happens. A cron job is idle until its minute arrives. The art of extensibility is making power cheap when unused — you can install a hundred of these and pay almost nothing for the ninety-nine that aren't firing right now. Let's build each surface, in order, and watch the cost stay where it belongs.

Why is "just paste everything the agent might need into the system prompt" the wrong way to extend a gateway?

Chapter 1: Skills — Load Knowledge on Demand

Start with the most common need: you want the agent to know how to do a thing — follow your deploy runbook, format a report your way, query a particular API. That's a skill: a small bundle of knowledge or procedure, written as a file (OpenClaw uses a SKILL.md), that the agent can read when the moment calls for it.

The first instinct is to drop every skill's full text into the prompt so the model always has it. We just saw why that's a trap. The clever move is the opposite: the prompt carries only a compact list — each skill's name, a one-line description, its file path, and a tiny version markernot the skills themselves. The model reads a skill's actual SKILL.md only when that skill looks relevant to the task at hand.

The compact list, not the contents. Think of the prompt's skill section as a table of contents, not the book. Each entry says "there is a skill called deploy-runbook, it's about shipping releases, it lives at this path, and its content is at version v3." That costs a line. The thousand-word runbook costs nothing until the agent, mid-task, thinks "I should follow the deploy procedure" and goes and reads the file. Power on the shelf, paid for only when taken down.

Why the version marker? Because the agent caches what it read. Once it has read deploy-runbook at v3, re-reading the same unchanged file every turn is wasted tokens. The marker is a cheap content fingerprint: if the list still shows v3, the agent trusts its memory of the skill; if the marker has flipped to v4, the file changed, and the agent re-reads it. Read once, re-read only on change. It's the same "don't redo unchanged work" idea you've seen in build systems and caches.

But not every skill should even appear in the list. A skill that shells out to ffmpeg is useless on a host without ffmpeg; a Windows-only procedure is noise on a Mac. So before a skill makes the list, it passes eligibility gates: required binaries or environment variables present, the right operating system, and the agent's own allowlist of what it's permitted to use. Fail a gate and the skill is invisible — it never even costs the agent a line of attention.

One more cap: the character budget. Even compact, a list of three hundred skills would itself bloat the prompt. So OpenClaw caps the rendered list with maxSkillsPromptChars. Eligible skills are added until the budget is spent; the rest are held back. Two filters in sequence, then: first eligibility (can this skill even work here?), then budget (does it fit in the list's space?). Only what survives both is offered to the model.

Let's implement exactly that selection — the function that turns a pile of installed skills into the short list the prompt actually carries, and marks which ones the agent must re-read.

The prompt's skill budget — eligible, fit, or dropped

Each block is an installed skill, sized by its character cost. Green = made the list (eligible AND fit the budget); red = dropped (ineligible — missing a binary — or overflowed). The bar at top is the prompt budget filling up. Press Bump version to change a loaded skill's content marker and watch it flip to reload; drag the budget to squeeze skills out.

budget (chars)1100
Drag the budget; bump a version; install a binary.
A skill's version marker in the prompt list flips from v3 to v4. What does the agent do, and why does the marker exist at all?

Chapter 2: Plugins — Bolting On Whole Subsystems

Skills extend what the agent knows. But sometimes you want to extend what the gateway is — give it a new chat app, a new tool, a different way of remembering. That's a plugin: a packaged subsystem that snaps into the gateway, either shipped in the box (bundled) or written by a third party.

Recall how much machinery a single surface needs: in Lesson 1, every chat app was wrapped in an adapter that translated its dialect into the gateway's one event vocabulary. A plugin is how you add a new one without touching the core. Want Matrix or Microsoft Teams? Install a channel plugin and the gateway gains that surface. The core never learns Matrix's quirks; the plugin does, and presents the same normalized events everything downstream already understands.

What a plugin can add. Plugins aren't limited to channels. A plugin can register:
  • extra channels — new surfaces like Matrix or Teams (Lesson 1's adapters, shipped as installable packages).
  • tools — new abilities the agent can call (Lesson 10's tool layer, extended).
  • compaction providers — alternative strategies for shrinking long context (Lesson 5's job, swapped out).
  • memory backends — e.g. a LanceDB vector store standing in for the default memory (Lesson 8's store, replaced).
  • workflow pipelines — multi-step processing stages the gateway runs.

Notice the pattern: a plugin can replace or augment nearly any seam you built in the earlier lessons. That power is exactly why plugins can't just reach in and monkey-patch the core — that way lies chaos. Instead, a plugin integrates through typed contracts. The gateway publishes a precise shape for each extension point: "a channel plugin must provide these functions with these argument types"; "a memory backend must implement save and search with these signatures." The plugin fills the contract; the core calls it without knowing or caring which plugin is behind it.

Plugin hooks: priorities and merge semantics. Because two plugins might both want to react to the same extension point, plugin integration carries two more ideas you'll formalize next chapter. A priority orders who runs first when several plugins attach to the same point. And a merge semantics rule decides how their contributions combine — do they all run in sequence, does the first match win, do their outputs get concatenated? Typed contracts say what a plugin may provide; priorities and merge rules say how competing providers coexist. Hold that thought — it's the whole subject of Chapter 4.

The big idea is that a plugin is a subsystem, not a snippet. A skill is a paragraph the agent might read; a plugin is a whole new organ grafted onto the gateway — a channel, a memory, a pipeline — integrated through a contract so the core stays clean. Skills grow the mind; plugins grow the body.

A third-party plugin wants to add a LanceDB vector store as the agent's memory backend. How does it integrate without the gateway core having to know about LanceDB?

Chapter 3: Hooks — Your Code on the Gateway's Events

Skills and plugins extend what the agent knows and is. Hooks extend what the gateway does at specific moments. A hook is a piece of code that the gateway runs automatically when a particular event happens — a message arrives, a session is about to be compacted, the gateway is starting up. You don't call a hook; the event does.

This is the classic event-driven pattern: instead of one giant procedure with every concern tangled together, the gateway emits named events at well-defined points, and independent bits of code subscribe to the events they care about. The agent loop doesn't need to know that something wants to log every command; the command-logger hook just listens for the right event.

The event families. OpenClaw fires hooks across the whole lifecycle:
  • commandscommand:new, command:reset, command:stop (the user starts, resets, or stops a turn).
  • session compactionsession:compact:before and session:compact:after (around the context-shrinking step from Lesson 5).
  • agentagent:bootstrap (an agent run is being set up — the moment to inject context).
  • gateway lifecyclegateway:startup and gateway:shutdown (the daemon coming up or going down).
  • messagesmessage:received, message:transcribed, message:sent (a message moving through its stages).

What do you do with these? OpenClaw ships a set of bundled hooks that are themselves good worked examples of the pattern:

The bundled hooks.
  • session-memory — on command:new / command:reset, saves the recent messages so a fresh turn doesn't lose the thread.
  • bootstrap-extra-files — on agent:bootstrap, injects files matching a glob (e.g. all your notes) into the agent's starting context.
  • command-logger — records each command as it happens, for audit and debugging.
  • compaction-notifier — tells you (around session:compact) when a long context just got summarized, so a sudden "memory shift" isn't mysterious.
  • boot-md — on gateway:startup, runs the instructions in a BOOT.md file — your gateway's start-of-day routine.

See how each bundled hook is just "small code, fired on the right event." None of them required changing the agent loop or the protocol; they hang off events the core already emits. That's the gift of an event system — the core stays one clean spine, and behavior accretes at the edges, one independent listener at a time. The only question left, once you have many listeners, is which one wins when two attach to the same event. That's next.

The bundled boot-md hook runs your BOOT.md instructions when the daemon comes up. Which event does it listen for, and what general pattern does it exemplify?

Chapter 4: Hook Discovery & Precedence

Once anyone can attach a hook, a governance question appears: hooks come from several places, and two of them might define a hook with the same name. If both the gateway's bundled session-memory and a custom one in your workspace exist, which runs? You need a clear rule, or behavior becomes a coin flip.

OpenClaw resolves this with a precedence order over the four sources a hook can come from, lowest authority to highest:

The four sources, LOW → HIGH precedence.
  • 1. bundled — ships with OpenClaw. The baseline (lowest).
  • 2. plugin — contributed by an installed plugin.
  • 3. managed — from ~/.openclaw/hooks, your machine-wide hooks.
  • 4. workspace — from <workspace>/hooks, scoped to this project. The most specific (highest).
A higher-precedence source overrides a same-id hook from a lower one. So a session-memory hook in your workspace replaces the bundled one of the same id — broad defaults, narrow overrides.

This is the same cascade you met for queue modes in Lesson 6 and for config everywhere: defaults at the bottom, the most specific scope winning at the top. Specificity is authority. Your project's hooks folder is the most specific thing about this project, so it gets the final say — you can ship a custom session-memory that behaves exactly how this codebase needs, without forking the gateway.

Discovery is OFF by default. A crucial safety choice: the gateway does not go hunting for hooks to run unless you opt in. You activate hooks by explicitly enabling one, installing a hook pack, or setting hooks.internal.enabled. Running arbitrary code on every event is a powerful, dangerous thing — so the default is silence, and you turn on exactly what you want. (You've seen this stance before: the gateway binds to loopback by default, demands a handshake, and gates node capabilities. Off-by-default is the house style.)

So resolution has two stages. First, override by precedence: among hooks sharing an id, keep only the highest-precedence source's version; drop the rest. Then, among the survivors for a given event, dispatch in priority order — a per-hook priority number decides who runs first (the merge semantics from Chapter 2, made concrete). Let's implement exactly that resolver.

A session-memory hook is defined in both the bundled set and your workspace's hooks folder, for the same event. Which one fires?

Chapter 5: Cron — Tasks on a Schedule

So far every extension still waits for something to happen — a message, a startup, a command. The last surface flips that: cron jobs let the gateway act on the clock, with no human and no incoming message required. "Every morning at 8, summarize my unread mail." "Every hour, check the build status." Proactive, not reactive.

A cron job is specified with a cron expression — the venerable five-field format from Unix: minute, hour, day-of-month, month, day-of-week. Each field is a wildcard, an exact number, or a step. 0 8 * * * means "minute 0 of hour 8, every day" — 8:00 AM daily. */15 * * * * means "every 15 minutes." The expression is a tiny language for "when," and the gateway watches the clock and fires the job the instant a tick matches.

Two properties that make cron safe inside a gateway.
  • The cron lane (Lesson 6). Scheduled jobs run on a dedicated cron lane, separate from foreground chat. A heavy nightly digest can never starve the conversation you're having right now — the lanes proceed in parallel, each with its own concurrency budget.
  • A fresh session (Lesson 7). Each cron run gets a brand-new session, not your live chat's transcript. The job doesn't pollute your conversation's memory, and it starts clean every time — deterministic, isolated, repeatable.

There's a close cousin worth naming: the webhook. Where cron is triggered by the clock, a webhook is triggered by an external HTTP request — some other system POSTs to the gateway and kicks off a job. Same idea (run work without a chat message starting it), different trigger: time vs. an outside event. Both let the gateway do things the user never explicitly asked for in the moment.

Let's build the heart of cron: given a schedule and the current time, compute when the job fires next. We'll work inside a single day, in minutes, to keep the arithmetic clean — the wrap-to-tomorrow case is a detail on top.

A cron job runs your nightly digest. Why does each cron run get a fresh session on a dedicated cron lane, rather than running in your live chat?

Chapter 6: Heartbeats & Wake — The Agent's Own Cadence

Cron handles "do this at a fixed time." But there's a softer kind of proactivity: you want the agent to have a gentle pulse — to come alive periodically and check on things, even when nobody scheduled an exact job. That's the heartbeat.

A heartbeat is a periodic beat — we met the word back in Lesson 1, as the gateway's "I'm still alive" tick. Here it gains a second meaning: a recurring cadence on which the agent can act on its own initiative. On each beat the agent might glance at a long-running task, poll an inbox, or decide whether anything needs a proactive check-in. It's the difference between an assistant that only ever answers when spoken to and one that occasionally says "by the way, that build you started finished."

Heartbeat vs. wake. Two complementary mechanisms:
  • Heartbeat — a periodic cadence. Every so often, the agent gets a beat and may act. This is the steady metronome.
  • Wake — a nudge that says "wake up now," outside the regular beat. Some condition or signal rouses the agent immediately rather than waiting for the next tick.
Together they make the agent more than a pure request/response servant: it has a rhythm of its own, and it can be roused early when something can't wait.

Why does this matter architecturally? Because everything up to Lesson 10 made the agent fundamentally reactive — a message comes in, a reply goes out. Heartbeats and wake add a thin layer of agency: the agent isn't only a function you call; it's a process with its own cadence that can initiate. Cron is the rigid version (exact times); the heartbeat is the soft version (a living pulse); wake is the override (rouse it now). All three break the agent out of "speak only when spoken to."

The four reactive-to-proactive surfaces, lined up. Hooks fire on events (reactive, but automatic). Cron fires on exact times (proactive, rigid). Heartbeats fire on a periodic cadence (proactive, soft). Wake fires on a nudge (proactive, on demand). Skills and plugins, by contrast, change what the agent can do; these four change when it does it. A complete gateway needs both axes — new capabilities and new occasions to use them.
What does adding heartbeats and a wake signal change about the agent, compared to the purely message-driven gateway of Lessons 1–10?

Chapter 7: Putting It Together — Four Surfaces Cooperating

Each surface is useful alone, but the payoff is watching them cooperate. Let's trace a single realistic automation end to end — a daily briefing — and name which surface does each step.

It's 8 AM. A cron job fires (0 8 * * *), on the cron lane, in a fresh session — no human asked for anything. As the run spins up, an agent:bootstrap hook fires and injects today's notes and calendar into the starting context (exactly what the bundled bootstrap-extra-files hook does). Now the agent, set up and aware of the day, decides it should follow the briefing procedure — so it reads a skill, daily-briefing, on demand, pulling in the exact format and steps you want. It composes the summary and posts it to your channel.

8:00 AM — cron
0 8 * * * fires on the cron lane, fresh session
hook
agent:bootstrap injects today's notes & calendar
skill
agent reads daily-briefing on demand — the format
post
summary delivered to your channel (a plugin could add a new one)
Why this is the whole point. No single surface could do this cleanly. Cron alone gives you the 8 AM trigger but no context and no procedure. A hook alone reacts but never initiates. A skill alone is knowledge with no occasion to use it. A plugin alone is plumbing. Compose them and you get a genuinely autonomous routine: the right time (cron), the right context (hook), the right procedure (skill), through the right channel (plugin) — each loaded only when its moment arrives, the prompt and core never permanently weighed down.

That composition is the soul of extensibility. The four surfaces aren't four separate features bolted on; they're four complementary answers — new knowledge, new subsystems, new event-handlers, new occasions — that snap together into behaviors none of them could produce alone. Build all four and your gateway stops being a fixed program and becomes a platform.

In the 8 AM daily-briefing automation, what is each surface responsible for?

Chapter 8: Series Finale — The Whole Gateway

Here is the extension layer in motion, and behind it, the whole gateway you've built. Press the buttons to fire each surface and watch them cooperate on one timeline: a cron tick spawns a fresh-session job; a message arrives and its hooks fire in precedence order (workspace overriding bundled, drawn as a resolved stack); a skill loads on demand. Four surfaces, one event timeline — the payoff of the entire lesson, and the capstone of the series.

The extension playground — cron, hooks, skills cooperating

The center line is the gateway's event timeline. Cron ticks spawn jobs (fresh session). A message triggers hooks — shown resolving by precedence, with the workspace hook overriding the bundled one of the same id. A skill loads on demand. Press the buttons and read the running log.

Idle. Fire a surface to watch it act on the timeline.
The extension layer in one breath. Four surfaces grow the gateway without weighing down the core: skills ride in the prompt as a compact, eligibility-gated, budget-capped list and are read on demand, re-read only when a version marker changes (Ch 1); plugins add whole subsystems — channels, tools, memory backends — through typed contracts (Ch 2); hooks run your code on lifecycle events, resolved by precedence (bundled < plugin < managed < workspace) and dispatched by priority, off by default (Ch 3–4); and cron fires scheduled work on its own lane in a fresh session, with heartbeats and wake adding a softer proactive cadence (Ch 5–6). Composed, they yield autonomous routines no single surface could (Ch 7).

The whole series, in one breath

You built an entire agent gateway, end to end. Trace the spine: a single long-lived daemon owns the stateful connections (1) and speaks a strict wire protocol to its clients (2); it decides who may connect and what they may do through auth, pairing, and trust (3); on each message it runs the agent loop — intake, context, inference, tools, reply (4) — building the prompt and streaming the answer back out (5); a command queue with lanes keeps overlapping runs from colliding (6), routing many conversations to the right session and agent persona (7); memory gives it continuity across turns (8); it self-heals from its own crashes (9); tools and sandboxing let it act in the world safely (10); and finally skills, plugins, hooks, and cron let you extend every one of those layers without touching the core (11). Daemon to protocol to trust to loop to stream to queue to sessions to memory to self-healing to tools to extensions. That's the gateway.

Where to go next on this site

The Feynman test — the whole series. Close the lesson and explain to a friend, in one breath: the four extension surfaces and what each costs the prompt and core; why skills are a compact list with version markers and eligibility gates; how hooks resolve by precedence and dispatch by priority; what makes cron safe inside a gateway (its lane and fresh session); and then the full spine — daemon, protocol, trust, loop, stream, queue, sessions, memory, self-healing, tools, extensions. If you can walk that chain end to end, you don't just understand an agent gateway — you could build one.
In one sentence, what do skills, plugins, hooks, and cron together give the gateway?