u/BretTruchan

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.ts structurally.
  • resolveAllowFrom is under base.config.
  • account.enabled is set.
  • gateway.startAccount calls:
ctx.setStatus({ running: true, configured: true, enabled: true })

and parks on ctx.abortSignal.

  • I patched openclaw.json with:
{
  "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:

  1. Is there a manifest field, package.json field, or openclaw.json config key that promotes a discovered or installed plugin from “lazy/post-ready” to “boot-with-the-bundled-set”?

  2. 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.

  3. Is there a smaller third-party channel plugin you could point me at that successfully loads from the runtime extensions volume and has its startAccount called?

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.

reddit.com
u/BretTruchan — 11 days ago

Hello everyone! I create modules for VCV Rack under the brand "Voxglitch", and I'm preparing to launch a time-synced effects processor.

I'm seeking audio to demo in the effect. I'm open minded about the type of music, ideally something with a beat and without heavily applied effects.

In return, I'll mention your music in the demo video. My videos usually get around 100 views.

Thanks! Bret

reddit.com
u/BretTruchan — 15 days ago