Downcity
ServicesExamples

WebhookRelayService Scenario

Use a background-worker scenario to show what kinds of capabilities are obviously better as a service

WebhookRelayService Scenario

Scenario

You have an internal system that keeps pushing events into a local queue.
You want the agent process to own a relay worker that:

  • keeps reading events
  • does light normalization
  • forwards results into the rest of your application

At that point, the shape is very clearly a service, not a plugin or a tool.

Why this is not a tool

Because it needs:

  • long-lived execution
  • a background loop
  • an in-memory queue
  • clear start and stop boundaries

Simplified example

import {
  Agent,
  BaseService,
  type AgentContext,
  type ServiceActions,
} from "@downcity/agent";

class WebhookRelayService extends BaseService {
  readonly name = "webhook_relay";

  private timer: NodeJS.Timeout | null = null;
  private readonly events: Array<{ id: string; body: string }> = [];

  readonly lifecycle = {
    start: async (_context: AgentContext) => {
      if (this.timer) return;
      this.timer = setInterval(() => {
        const event = this.events.shift();
        if (!event) return;
        console.log("relay event", event.id, event.body);
      }, 500);
    },
    stop: async () => {
      if (!this.timer) return;
      clearInterval(this.timer);
      this.timer = null;
    },
  };

  enqueue(body: string) {
    this.events.push({
      id: `evt-${Date.now()}`,
      body: String(body || ""),
    });
  }

  readonly actions: ServiceActions = {
    status: {
      execute: async () => {
        return {
          success: true,
          data: {
            running: Boolean(this.timer),
            queued: this.events.length,
          },
        };
      },
    },
  };
}

const relayService = new WebhookRelayService();

const agent = new Agent({
  id: "relay-agent",
  path: "/path/to/project",
  tools: {},
  services: [relayService],
});

await agent.start({
  rpc: true,
});
relayService.enqueue("build finished");

The key lessons here

1. The real center of gravity is long-lived execution

not any one individual action.

2. The worker belongs to the service instance

not a module-level singleton that several agents could accidentally share.

3. App code often keeps a typed service reference directly

For example:

relayService.enqueue("build finished");

That is often more natural in the current SDK than pretending every internal capability should first become a generic action call.