Hi all,
I am building a small custom channel plugin, and I have hit a timing question that I think is probably obvious to anyone who has shipped a third-party channel before. Hoping someone can point me in the right direction.
What I am trying to do
I run a small SaaS where each customer gets their own self-hosted OpenClaw VM: one realtor per droplet, behind Traefik, single tenant.
I want a “Portal” channel inside our web app. The customer chats with the agent in our browser UI, and the agent’s replies stream back.
I am building this as a custom channel plugin called dealzen-portal. For V1, I just want the round trip to work end to end. The outbound sendText callback can stub to console.log for now, and I will wire the real HTTP webhook for inbound in V2.
I am on tag 2026.4.14. The plugin lives in:
~/.openclaw/extensions/dealzen-portal/
It is auto-discovered and copied in by my provisioning script, not installed via npm.
Where I am stuck
The plugin loads fine. I see this in the gateway logs:
[dealzen-portal] plugin loaded
But when I run:
openclaw agent --agent main --deliver --reply-channel dealzen-portal --reply-to test --message "hi"
The gateway accepts the request:
[ws] res ✓ agent 170ms
It runs the agent, and then the reply pipeline throws:
[delivery-recovery] Delivery ... hit permanent error: Outbound not configured for channel: dealzen-portal
My channel plugin’s gateway.startAccount is never invoked, so the channel never reaches running: true, and the outbound adapter never makes it into the active registry.
What I think the actual root cause is
Looking at the boot timeline:
00:00:00 [gateway] ready (5 plugins: acpx, browser, device-pair, phone-control, talk-voice; 14s)
00:00:30 [plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load: dealzen-portal
00:00:55 [dealzen-portal] plugin loaded
My plugin loads about 55 seconds after [gateway] ready.
The bundled channel plugins, such as telegram, discord, qa-channel, etc., live at:
/app/extensions/
inside the image, and are in that initial “5 plugins” set.
Mine lives at:
~/.openclaw/extensions/
the runtime volume, and seems to be evaluated lazily after boot.
By the time my plugin registers its channel, the orchestrator has already snapshotted its active channel set, so my channel never enters the lifecycle that calls startAccount.
What I have already tried
I want to save anyone helping the time of suggesting things I have already verified:
- The plugin code matches
extensions/qa-channel/src/channel.tsstructurally. resolveAllowFromis underbase.config.account.enabledis set.gateway.startAccountcalls:
ctx.setStatus({ running: true, configured: true, enabled: true })
and parks on ctx.abortSignal.
- I patched
openclaw.jsonwith:
{
"channels": {
"dealzen-portal": {
"enabled": true,
"dmPolicy": "open"
}
},
"plugins": {
"allow": ["dealzen-portal"]
}
}
- I ran:
openclaw plugins install /tmp/dealzen-portal-stage
so there is a real install record in ~/.openclaw/openclaw.json under:
plugins.entries.dealzen-portal.enabled = true
plugins.installs.dealzen-portal
- After restart, the gateway-ready line still says “5 plugins,” and my plugin still loads about 55 seconds later.
- I tried this in the manifest:
{
"activation": {
"onStartup": true
}
}
No change. None of the bundled plugins I checked have an activation field, so I think this flag is not the gating mechanism on this version.
- The plugin installs cleanly past the dangerous-code static check.
My specific question
What is the actual mechanism that promotes a custom plugin to the synchronous boot phase, so that its channel ends up in the orchestrator’s initial active set and startAccount actually gets called?
Concretely, on tag 2026.4.14:
Is there a manifest field,
package.jsonfield, oropenclaw.jsonconfig key that promotes a discovered or installed plugin from “lazy/post-ready” to “boot-with-the-bundled-set”?If not, is the official path for shipping a custom channel plugin to bake it into
/app/extensions/via the Docker image at build time? I can do this, I just want to confirm it is the documented approach before going down that road.Is there a smaller third-party channel plugin you could point me at that successfully loads from the runtime extensions volume and has its
startAccountcalled?
I have read qa-channel, telegram, and soimy/openclaw-channel-dingtalk. The first two are bundled, and the third is large enough that I have not been able to isolate the relevant difference.
Thank you for any pointers. This has been a fun system to learn, but I have hit the edge of what I can figure out from the source alone.