Skip to content
PricingBlog
✨ Markdown

Signals

Signals are lifecycle controls for entities. They let users and hosts interrupt active work, pause or resume entities, kill entities, and deliver custom lifecycle notifications to handlers.

Signal records are written to the entity's signals collection and appear in timeline helpers.

Supported signals

SignalRuntime behavior
SIGINTAbort the active handler invocation through ctx.signal. Use for "stop current run".
SIGSTOPRuntime-controlled pause.
SIGCONTRuntime-controlled resume.
SIGKILLTerminal kill/delete signal.
SIGHUPDelivered to ctx.onSignal() handlers.
SIGTERMDelivered to ctx.onSignal() handlers for graceful shutdown-style behavior.
SIGUSRDelivered to ctx.onSignal() handlers for application-defined behavior.

Runtime-controlled signals are handled by the runtime and are not delivered to ctx.onSignal().

CLI

Send a signal from the CLI:

sh
electric agents signal /horton/onboarding SIGINT --reason "stop current run"
electric agents signal /horton/onboarding SIGUSR --payload '{"refresh":true}'

kill is shorthand for a terminal signal:

sh
electric agents kill /horton/onboarding

Programmatic clients

Use createAgentsClient() for UI-style clients:

ts
const client = createAgentsClient({
  baseUrl: "http://localhost:4437",
  principalKey: "user:sam",
})

await client.signal({
  entityUrl: "/horton/onboarding",
  signal: "SIGINT",
  reason: "User clicked stop",
})

await client.kill("/horton/onboarding", "User deleted the session")

Use createRuntimeServerClient() when you need the lower-level server client:

ts
await runtimeClient.signalEntity({
  entityUrl: "/worker/analysis",
  signal: "SIGUSR",
  payload: { refresh: true },
})

The caller needs signal permission on the entity, or manage.

Handler cancellation

Every handler receives ctx.signal, an AbortSignal that fires when the current wake should stop early. Pass it to cancellable work:

ts
async handler(ctx) {
  const res = await fetch("https://api.example.com/data", {
    signal: ctx.signal,
  })

  await ctx.sandbox.exec({
    command: "npm test",
    signal: ctx.signal,
    timeoutMs: 60_000,
  })
}

SIGINT aborts this signal. Runtime shutdown can also abort it.

Handler-delivered signals

Use ctx.onSignal() for SIGHUP, SIGTERM, and SIGUSR:

ts
async handler(ctx) {
  ctx.onSignal(async ({ signal, reason, payload }) => {
    if (signal === "SIGUSR") {
      ctx.insertContext("refresh-request", {
        name: "Refresh request",
        content: JSON.stringify({ reason, payload }),
        attrs: {},
      })
    }
  })

  await ctx.agent.run()
}

Handlers should keep signal callbacks short and idempotent. If the signal should trigger substantial work, record state or context and let the normal handler flow pick it up.

Signal records

Signal rows include the signal name, sender, reason, payload, handling status, outcome, and state transition fields:

ts
interface Signal {
  key: string
  signal: "SIGINT" | "SIGHUP" | "SIGTERM" | "SIGKILL" | "SIGSTOP" | "SIGCONT" | "SIGUSR"
  status: "unhandled" | "handled"
  sender?: string
  reason?: string
  payload?: unknown
  timestamp: string
  handled_at?: string
  handled_by?: string
  outcome?: "transitioned" | "ignored" | "invalid_for_state" | "delivered" | "aborted" | "shutdown_requested" | "failed"
  previous_state?: ChildStatusEntry["status"]
  new_state?: ChildStatusEntry["status"]
}

See Built-in collections for the full row shape.