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
| Signal | Runtime behavior |
|---|---|
SIGINT | Abort the active handler invocation through ctx.signal. Use for "stop current run". |
SIGSTOP | Runtime-controlled pause. |
SIGCONT | Runtime-controlled resume. |
SIGKILL | Terminal kill/delete signal. |
SIGHUP | Delivered to ctx.onSignal() handlers. |
SIGTERM | Delivered to ctx.onSignal() handlers for graceful shutdown-style behavior. |
SIGUSR | Delivered 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:
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:
electric agents kill /horton/onboardingProgrammatic clients
Use createAgentsClient() for UI-style clients:
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:
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:
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:
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:
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.