#Webhooks
73 messages ¡ Page 1 of 1 (latest)
Yep, basically, but not as a bare heartbeat.
POST /hooks/wake does 2 things:
- enqueues a system event on the main session
- optionally wakes heartbeat
mode: "now"= run it immediatelymode: "next-heartbeat"= wait for the next scheduled heartbeat
The text field is the reason/context for the wake. It gets injected into the prompt as a System: line, so the agent knows why it was woken.
So this:
{ "text": "New email received", "mode": "now" }
means:
wake up now, and tell the agent âNew email receivedâ
If you want a one-off isolated agent run instead of âpoke the main session + heartbeatâ, use /hooks/agent.
Docs:
Glad that helped! đ If this fixed it, please right-click the solution message â Apps â â Mark Solution.
with /hooks/agent I can't seem to send direct dm through discord. Any tips?
A few gotchas there.
For Discord DMs with /hooks/agent, Iâd try an explicit target, not channel: "last":
{
"message": "test dm",
"name": "Webhook test",
"channel": "discord",
"to": "user:417043254136012801"
}
Main things to check:
- Use
user:<id>, not a bare numeric ID.
Discord DM delivery expectsuser:<id>or a mention like<@id>. Bare IDs are ambiguous and usually rejected.
- Donât rely on
lastif you want a DM.
lastwill try to reuse the previous route, which is often the current thread/channel, not a DM. - Make sure Discord DMs are actually allowed:
channels.discord.dm.enabledis notfalsechannels.discord.dmPolicyis not"disabled"- if youâre using pairing mode, that user may need to be paired/allowed first
- the user must allow DMs from server members / share a server with the bot
So the most likely fix is: set channel: "discord" and to: "user:<id>" explicitly.
Docs:
If that still fails, send:
openclaw --version- the exact JSON youâre POSTing
- the error/log line
Glad that helped! đ If this fixed it, please right-click the solution message â Apps â â Mark Solution.
can you send me full list of json properties that I can send with the hook? the "channel" and "to" aren't mentioned in the docs
Yep. From current source, these are the JSON fields actually read.
POST /hooks/wake
{
"text": "New email received",
"mode": "now"
}
Fields:
textrequiredmodeoptional,"now"or"next-heartbeat"(default:"now")
POST /hooks/agent
{
"message": "Summarize inbox",
"name": "Email",
"agentId": "main",
"idempotencyKey": "abc123",
"wakeMode": "now",
"sessionKey": "hook:email",
"deliver": true,
"channel": "discord",
"to": "user:417043254136012801",
"model": "openai/gpt-5.4-mini",
"thinking": "medium",
"timeoutSeconds": 60
}
Fields:
messagerequirednameoptional, default:"Hook"agentIdoptionalidempotencyKeyoptionalwakeModeoptional,"now"or"next-heartbeat"(default:"now")sessionKeyoptional, but only works ifhooks.allowRequestSessionKey=truedeliveroptional, default:truechanneloptional, default:"last"- valid values are
"last"plus installed channel ids like"discord","telegram", etc.
- valid values are
tooptional, delivery target- for Discord DM, use
user:<id>or<@id>
- for Discord DM, use
modeloptionalthinkingoptionaltimeoutSecondsoptional, positive integer
A couple of gotchas:
channel+toare only meaningful when you want explicit delivery.- If you omit
channel, it defaults to"last". - If you send a
sessionKeyand havenât enabledhooks.allowRequestSessionKey, it gets rejected. - Extra JSON keys are ignored unless youâre using a mapped hook.
Docs on current main do list channel and to under /hooks/agent, so if youâre not seeing that, Iâm a little suspicious youâre on older docs or an older release. What does openclaw --version show?
Refs:
so sessionKey is just for filtering, not for actual message delivery?
Not just filtering.
sessionKey is mainly for which session context the isolated run uses. That means:
- it decides which transcript/history the hook run reads/writes
- it can indirectly affect delivery when you use
channel: "last"(or omitchannel/to), because OpenClaw can reuse that sessionâs storedlastChannel,lastTo, and thread context
So:
sessionKey= which session am I running in?channel+to= where should the output be delivered?
If you set explicit delivery like:
{
"sessionKey": "hook:email",
"channel": "discord",
"to": "user:417043254136012801"
}
then channel/to win for delivery.
If you do this instead:
{
"sessionKey": "hook:email",
"channel": "last"
}
then delivery can be derived from whatever that session last talked to.
One more gotcha: external /hooks/agent requests can only set sessionKey if hooks.allowRequestSessionKey=true.
Docs:
If you want, I can give you a tiny âmental modelâ table for sessionKey vs channel vs to vs wakeMode.
so event hooks/agent has wakeMode? what it's for? I thought you said hooks/agent will be run as isolated agent? will it be a heartbeat?
Yeah, the name is a bit confusing đ
/hooks/agent is not a heartbeat run. It does an isolated agent turn.
wakeMode there is only about what happens after that isolated run, if OpenClaw needs to surface the result back to the main session:
wakeMode: "now"
enqueue a summary/error into the main session and wake heartbeat immediatelywakeMode: "next-heartbeat"
enqueue the summary/error into the main session, but wait for the next normal heartbeat
So the mental model is:
/hooks/wake= âpoke the main session / heartbeatâ/hooks/agent= ârun a separate isolated agent jobâwakeModeon/hooks/agent= âif that isolated job needs to notify the main session, should that notification wake heartbeat now or later?â
One more nuance: if the isolated /hooks/agent run is already delivered externally (for example via explicit channel/to), then there may be nothing to wake at all.
Docs:
If you want, I can sketch the exact flow for:
/hooks/wake/hooks/agentwith delivery/hooks/agentwithout delivery
yes please
Sure, hereâs the exact mental model:
1) /hooks/wake
Purpose: poke the main session
Flow:
- your POST sends
{ text, mode } - OpenClaw enqueues
textas a system event on the main session - if
mode: "now", it triggers heartbeat immediately - if
mode: "next-heartbeat", it waits for the next scheduled heartbeat
So this is basically:
âmain session, wake up, hereâs whyâ
2) /hooks/agent with delivery
Purpose: run a separate isolated agent turn and send the result out directly
Example:
{
"message": "Summarize this alert",
"channel": "discord",
"to": "user:417043254136012801"
}
Flow:
- OpenClaw starts an isolated agent run
- that run generates a reply
- reply is delivered directly to the target (
channel+to) - since it was already delivered, there may be nothing to wake heartbeat for
So this is basically:
ârun this separately, and send the result thereâ
3) /hooks/agent without delivery
Example:
{
"message": "Check this webhook payload",
"deliver": false,
"wakeMode": "now"
}
Flow:
- OpenClaw starts an isolated agent run
- it finishes, but does not deliver externally
- OpenClaw writes a short summary/error back to the main session as a system event
wakeModedecides whether the main session heartbeat runs now or later
So this is basically:
ârun this separately, then tell the main session what happenedâ
Short version
/hooks/wake= wake main session/hooks/agent= run isolated agentwakeModeon/hooks/agent= controls follow-up notification to main session, not whether the isolated run itself becomes a heartbeat
Docs: https://docs.openclaw.ai/automation/cron-jobs#webhooks
Glad that helped! đ If this fixed it, please right-click the solution message â Apps â â Mark Solution.
so if I want to call an agent in my discord dm session (so it will get all current context of the dm session), I'll use
/hooks/agent
sessionKey: <dm session key>
agentId: "main"
channel: "discord"
to: <dm user>
correct?
Not quite.
If your goal is:
run in the existing Discord DM session and get all current DM context/history
then /hooks/agent is the wrong one.
Why: /hooks/agent always creates an isolated agent turn. In current source it sets sessionTarget: "isolated", which means it starts a fresh session for that run. So passing the DM sessionKey does not make it continue the live DM transcript the way a normal DM message would.
So:
This is correct for:
an isolated run, delivered to your DM
{
"agentId": "main",
"channel": "discord",
"to": "user:<your_discord_id>"
}
This is not correct for:
âuse my current DM conversation history/contextâ
Even if you add:
"sessionKey": "<dm session key>"
that still does not turn /hooks/agent into âcontinue this chat normallyâ.
Short version
- want existing DM context â use the normal session/chat path, not
/hooks/agent - want isolated job + send result to DM â use
/hooks/agent
Iâd also avoid pointing /hooks/agent at a live DM session key unless you really know why you need it.
Docs: https://docs.openclaw.ai/automation/cron-jobs#webhooks
If you want, I can help map your exact goal to the right endpoint:
- âcontinue my DM conversationâ
- ârun isolated, but send result to DMâ
- âpoke the main session from a webhookâ
so there is no way to externally trigger an agent round in a session, with continuity?
Yes, but not via /hooks/agent.
The clean split is:
/hooks/*= simple external HTTP triggers/hooks/wakeâ poke main session/hooks/agentâ run isolated turn
- Gateway session APIs = continue an existing session with continuity
So if you want:
externally trigger a run in an existing session, with its history/context
use the normal session/chat surface, for example:
chat.sendwith the targetsessionKey- or
sessions.sendinto that existing session
- or the lower-level
agentRPC withsessionKey
That preserves continuity.
So the answer is:
- through hooks: no, not today
- through Gateway RPC/session APIs: yes
If you specifically want a webhook-shaped HTTP endpoint that continues a live session, I donât think thereâs a built-in /hooks/... route for that right now. Youâd usually put a tiny proxy in front of chat.send / sessions.send.
Docs:
- https://docs.openclaw.ai/concepts/agent-loop
- https://docs.openclaw.ai/gateway/protocol
- https://docs.openclaw.ai/automation/cron-jobs#webhooks
If you want, I can sketch the exact request shape youâd use for âcontinue this DM session via chat.sendâ.
from your 3 proposed solutions using normal chat surface, can you tell me the difference?
is there any option the will be displayed as my chat message in discord, and is there one that won't?
Yep. Short version:
| Option | Keeps session continuity? | Sends anything to Discord? | Can appear as your Discord message? |
| --------------------- | ------------------------- | -------------------------- | ----------------------------------- |
| sessions.send | yes | no | no |
| chat.send | yes | optional | no |
| agent with sessionKey | yes | optional | no |
What each one is for
**sessions.send**
⢠easiest âsend a message into this existing sessionâ
⢠basically a convenience wrapper around chat.send
⢠internal/session only
⢠good when you want continuity but no external Discord post
**chat.send**
⢠the normal chat/session API
⢠keeps continuity for that sessionKey
⢠can be internal only by default
⢠if you set deliver: true, the assistant reply can go out to Discord using the sessionâs route or an explicit originating route
**agent**
⢠lower-level run API
⢠also keeps continuity if you pass sessionKey
⢠gives you more knobs like model/provider/channel/to/replyTo/bestEffortDeliver
⢠best when youâre building a custom integration/client
Important Discord answer
If you mean:
âcan OpenClaw make it look like I wrote the Discord message?â
Then no, not through these APIs. OpenClaw wonât spoof your user message on Discord.
What can happen is:
⢠internal only: nothing shows in Discord
⢠external delivery: the bot/assistant reply shows in Discord
But the injected user turn is just in the session transcript, not a Discord message authored by you.
So if you want:
⢠continuity, no Discord output â sessions.send (or chat.send without deliver)
⢠continuity, plus bot reply in Discord â chat.send with deliver: true or agent with delivery
⢠a real message from your own Discord account â you have to actually send it yourself
Docs:
⢠https://docs.openclaw.ai/gateway/protocol
⢠https://docs.openclaw.ai/concepts/agent-loop
If you want, I can give you 3 tiny example payloads:
- silent continuation
- continue + bot replies in DM
- continue + strict explicit Discord route
yes please
Yep. Here are the 3 minimal examples.
These are Gateway RPC method params, not /hooks/* payloads.
Also, for all 3:
- they keep continuity
- none of them will show up as your Discord message
- only #2 and #3 can result in a bot reply showing in Discord
1) Silent continuation, no Discord output
Use sessions.send
{
"key": "agent:main:discord:direct:user:417043254136012801",
"message": "Summarize the last few messages into 3 bullets."
}
What happens:
- continues that session
- runs the agent
- stays internal/session-only
- nothing is posted to Discord
2) Continue session, let the bot reply into the DM
Use chat.send
{
"sessionKey": "agent:main:discord:direct:user:417043254136012801",
"message": "Reply to the latest question in one paragraph.",
"deliver": true,
"idempotencyKey": "demo-chat-send-1"
}
What happens:
- continues that session
- if the session already has a valid Discord DM route, the assistant reply gets delivered there
- still not shown as your message
3) Continue session, but force a strict explicit Discord DM route
Use agent
{
"agentId": "main",
"sessionKey": "agent:main:discord:direct:user:417043254136012801",
"message": "Answer the latest DM using the existing context.",
"deliver": true,
"channel": "discord",
"to": "user:417043254136012801",
"bestEffortDeliver": false,
"idempotencyKey": "demo-agent-1"
}
What happens:
- continues that session
- explicitly targets that Discord DM
- if routing is wrong/unavailable, it fails instead of quietly downgrading
- again, only the bot reply appears in Discord
Rule of thumb
sessions.send= simplest, silentchat.send= session-native, optional deliveryagent= most control, explicit routing knobs
Docs:
If you want, I can also write the exact WS envelope for one of these, like:
{ "id": 1, "method": "chat.send", "params": { ... } }
how can a third party script call "chat.send"?
Through the Gateway WebSocket protocol, not via a simple /hooks/*-style HTTP POST.
In practice
A third-party script has to:
- connect to the Gateway WS
- handle the
connect.challenge - send a
connectrequest with:- gateway auth (
tokenorpassword) - device identity + signed nonce
- role/scopes, usually
operator.read+operator.write
- gateway auth (
- then send a normal RPC request for
chat.send
Minimal shape
1) Gateway sends challenge
{
"type": "event",
"event": "connect.challenge",
"payload": { "nonce": "..." }
}
2) Script sends connect
{
"type": "req",
"id": "1",
"method": "connect",
"params": {
"minProtocol": 3,
"maxProtocol": 3,
"client": {
"id": "my-script",
"version": "0.1.0",
"platform": "linux",
"mode": "operator"
},
"role": "operator",
"scopes": ["operator.read", "operator.write"],
"auth": { "token": "YOUR_GATEWAY_TOKEN" },
"device": {
"id": "device_fingerprint",
"publicKey": "...",
"signature": "...",
"signedAt": 1737264000000,
"nonce": "..."
}
}
}
3) Then call chat.send
{
"type": "req",
"id": "2",
"method": "chat.send",
"params": {
"sessionKey": "agent:main:discord:direct:user:417043254136012801",
"message": "Reply using the existing DM context.",
"deliver": true,
"idempotencyKey": "demo-chat-send-1"
}
}
Important gotcha
This is a bit heavier than a normal webhook because raw WS clients must implement device auth/pairing, not just paste the gateway token.
So the practical answer is:
- possible: yes
- easy from a random script: not really
- easiest pattern: write a tiny local bridge that speaks Gateway WS once, then expose your own simple HTTP endpoint to your external script
Also, there is no built-in simple HTTP /chat.send endpoint like /hooks/wake.
Docs:
If you want, I can sketch a tiny Node/Python bridge for:
POST /continue-session -> chat.send
yes
Cool. The practical bridge Iâd build is this:
Easiest bridge: local HTTP -> openclaw agent
This avoids implementing raw Gateway WS auth/device pairing yourself.
Tiny Node example
import http from "node:http";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const server = http.createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/continue-dm") {
res.writeHead(404);
return res.end("not found");
}
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", async () => {
try {
const { userId, message, deliver = true } = JSON.parse(body);
if (!userId || !message) {
res.writeHead(400, { "content-type": "application/json" });
return res.end(JSON.stringify({ error: "userId and message are required" }));
}
const args = [
"agent",
"--agent", "main",
"--channel", "discord",
"--to", `user:${userId}`,
"--message", message,
"--json"
];
if (deliver) args.push("--deliver");
const { stdout, stderr } = await execFileAsync("openclaw", args);
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({
ok: true,
stdout: stdout ? JSON.parse(stdout) : null,
stderr: stderr || null
}));
} catch (err) {
res.writeHead(500, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: false, error: String(err) }));
}
});
});
server.listen(8787, "127.0.0.1", () => {
console.log("bridge listening on http://127.0.0.1:8787");
});
Call it like this
curl -X POST http://127.0.0.1:8787/continue-dm \
-H 'Content-Type: application/json' \
-d '{
"userId": "417043254136012801",
"message": "Reply using the current DM context",
"deliver": true
}'
What this gives you
- same DM route, so you get continuity
- if
deliver: true, the bot reply goes back to Discord - if
deliver: false, it stays internal/silent
When to use raw chat.send instead
Only if you specifically need:
- direct
sessionKeycontrol - a persistent WS client
- richer session APIs than the CLI wrapper gives you
Otherwise, this bridge is way simpler.
Docs:
If you want, I can also give you:
- a Python version, or
- a raw WebSocket
chat.sendexample instead
but with cli will the agent turn be isolated?
Nope, not isolated in the /hooks/agent sense.
openclaw agent is a normal agent turn. It resolves a real session via:
--to--session-id- or
--agent
and then runs with continuity for that session.
So if you do something like:
openclaw agent \
--agent main \
--channel discord \
--to user:417043254136012801 \
--message "Reply using the current DM context" \
--deliver
that is meant to behave like:
âsend a normal turn into that routed sessionâ
not:
âspin up an isolated throwaway runâ
Thatâs exactly the difference vs /hooks/agent, which is always isolated.
Only caveat: continuity depends on you targeting the same session route consistently. If you change the selector, youâll hit a different session.
Docs:
If you want, I can help translate:
- your DM session key
- into the exact
openclaw agentCLI invocation youâd want
I'm sending via cli like this
openclaw agent
--agent main
--session-id "discord:direct:417043254136012801"
--reply-channel discord
--reply-to "user:417043254136012801"
--reply-account main
--deliver
--message "[message]"
but it keeps going to agent:main:main session.
Ah, yep, the problem is this part:
--session-id "discord:direct:417043254136012801"
That is not a session id. Thatâs a session key / route-like string.
openclaw agent --session-id expects the actual internal sessionId (usually a generated id), not a Discord route.
So your command falls back to the normal --agent main target, which is why it lands in agent:main:main.
Two important fixes
- Use
--to, not--session-id, for route-derived targeting --reply-accountshould probably bedefault, notmain
(mainis your agent id, not the Discord account id)
Try this instead
openclaw agent \
--agent main \
--to "user:417043254136012801" \
--reply-channel discord \
--reply-to "user:417043254136012801" \
--reply-account default \
--deliver \
--message "[message]"
Important nuance
- this uses route-derived continuity
- it does not pin an exact
sessionKeystring the way rawchat.send/sessions.sendcan
So:
- if you want âtarget the exact existing session keyâ â use
chat.send/sessions.send - if you want âCLI, same DM route, normal continuityâ â use
openclaw agent --to ...
And yep, per docs:
--session-id= explicit session id--to= recipient used to derive the session key- reply flags only affect delivery, not session routing
Docs:
If you want, I can help you map your actual Discord DM session key to the best of:
- exact
chat.send - route-derived
openclaw agent
why does --channel discord --to user:417043254136012801 resolve to agent:main:main instead of agent:main:discord:direct:417043254136012801? Is there a flag we're missing, or is this a bug?
I checked the resolver, and this looks like current behavior, not a missing flag.
Why it happens
The CLI path behind openclaw agent does this for --to:
register.agent.tsstill describes--toas E.164 / phone-style routing- the session resolver in
src/config/sessions/session-key.tsexplicitly says:
All non-group direct chats collapse to this bucket (default:
main), groups stay isolated.
So for a direct target like:
--to user:417043254136012801
it resolves to the canonical direct-chat bucket:
agent:main:main
not:
agent:main:discord:direct:417043254136012801
So, bug or not?
Iâd call it a limitation / mismatch in expectations, and maybe a docs/CLI ergonomics bug.
- Not a missing flag, as far as I can see
- Not channel-aware DM routing in the current CLI resolver
- Yes, misleading if you expect Discord DM session continuity from
--to
What to use instead
If you need the exact Discord DM session key, use Gateway RPC:
chat.sendwithsessionKey- or
sessions.sendwithkey
Because those let you target:
agent:main:discord:direct:user:417043254136012801
directly.