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.
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.
Here is the shape of the four answers, each loading its cost only when needed:
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.
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 marker — not the skills themselves. The model reads a skill's actual SKILL.md only when that skill looks relevant to the task at hand.
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.
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.
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.
v3 to v4. What does the agent do, and why does the marker exist at all?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.
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.
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.
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.
command:new, command:reset, command:stop (the user starts, resets, or stops a turn).session:compact:before and session:compact:after (around the context-shrinking step from Lesson 5).agent:bootstrap (an agent run is being set up — the moment to inject context).gateway:startup and gateway:shutdown (the daemon coming up or going down).message: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:
command:new / command:reset, saves the recent messages so a fresh turn doesn't lose the thread.agent:bootstrap, injects files matching a glob (e.g. all your notes) into the agent's starting context.session:compact) when a long context just got summarized, so a sudden "memory shift" isn't mysterious.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.
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?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:
~/.openclaw/hooks, your machine-wide hooks.<workspace>/hooks, scoped to this project. The most specific (highest).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.
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.
session-memory hook is defined in both the bundled set and your workspace's hooks folder, for the same event. Which one fires?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.
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.
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."
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."
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.
0 8 * * * fires on the cron lane, fresh sessionagent:bootstrap injects today's notes & calendardaily-briefing on demand — the formatThat 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.
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 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.
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.