Skip to main content

Agent signals

Agent Signals are WednesdayAI’s lean plugin surface for non-user stimuli. They let a plugin publish structured meta-input — watcher output, commitment reminders, inbox summaries, context hints, background findings — without pretending the input came from a human conversation. Keep domain behaviour in your plugin. Core only owns flow control, dedupe, admission, and heartbeat dispatch.

Modes

The current implementation supports two modes:
  • context — stores the signal for plugin subscribers and future context collection. It does not wake a model turn.
  • heartbeat — stores the signal, queues a session-scoped system-event summary, and requests a targeted heartbeat wake.
The background, notify, and steer modes are reserved for future plumbing and currently return unsupported-mode.

Publishing a signal

Plugins access the API through api.runtime.signals. publish() is synchronous and never calls the model directly:
const result = api.runtime.signals.publish({
  type: "commitment.due",
  source: "my-plugin",
  sessionKey: "agent:main:telegram:direct:user-id",
  mode: "heartbeat",
  summary: "Review the follow-up promised to the operations team.",
  dedupeKey: "commitment:ops-follow-up:2026-05-26",
  ttlMs: 30 * 60 * 1000,
  payload: { commitmentId: "ops-follow-up" },
});

if (!result.accepted && result.reason !== "duplicate") {
  api.logger.warn("signal was not accepted", { reason: result.reason });
}
publish() returns either { accepted: true, signalId } or { accepted: false, reason }, where reason is one of duplicate, expired, queue-full, invalid, or unsupported-mode.

Signal fields

FieldTypeNotes
typestringRequired. Signal type, e.g. commitment.due.
sourcestringRequired. Your plugin id.
payloadunknownRequired. Structured data (kept out of transcripts by default).
mode"context" | "heartbeat"Dispatch mode.
summarystringConcise prompt-visible summary (max ~500 chars).
sessionKey / agentIdstringTarget session/agent.
priority"low" | "normal" | "high" | "urgent"Eviction ranking.
dedupeKeystringSuppresses duplicates against the same target.
ttlMsnumberExpiry for stale alerts.
requiresIdlebooleanDefer dispatch while the agent is busy.

Available helpers

api.runtime.signals.publish(signal);
api.runtime.signals.subscribe(filter, handler);
api.runtime.signals.getAvailability({ agentId, sessionKey });
api.runtime.signals.requestWake({ signalId });

Heartbeat dispatch

Heartbeat-mode signals bridge into the existing heartbeat system:
  1. The signal is accepted into a bounded in-memory store.
  2. Core enqueues a system event on the target sessionKey.
  3. Core requests requestHeartbeatNow({ reason: "signal:<type>", sessionKey }).
  4. The heartbeat runner decides whether to run, skip, or retry based on active hours, heartbeat config, and lane/session activity.
  5. When the heartbeat runs, the signal summary is framed as signal-event context so isolated heartbeat sessions still see the base-session content.
Signal dispatch does not bypass heartbeat isolation or skipWhenBusy.

Prompt visibility

Signals are meta-input, not user messages. Core exposes only the summary to the prompt-visible system-event queue:
Signal (my-plugin/commitment.due): Review the follow-up promised to the operations team.
Raw signal payloads are not written into normal transcripts by default, and should not be treated as user-authored memory unless your plugin explicitly opts in through its own hooks.

Availability

Use getAvailability() to decide whether to publish now, defer, or drop:
const availability = api.runtime.signals.getAvailability({ sessionKey });

if (!availability.canWakeHeartbeat) {
  api.logger.debug("deferring signal until the agent is less busy", {
    mainBusy: availability.mainBusy,
    sessionBusy: availability.sessionBusy,
    cronBusy: availability.cronBusy,
    subagentBusy: availability.subagentBusy,
    nestedBusy: availability.nestedBusy,
  });
}
Availability is advisory — the heartbeat runner remains the final admission gate. A signal with requiresIdle: true is held while availability reports busy; re-attempt with requestWake() later.

Dedupe and bounds

The in-memory store applies conservative limits:
  • 100 signals globally; 20 per target session, agent, or source.
  • Duplicate suppression by dedupeKey, scoped to the target session/agent/source (the same dedupeKey against two different targets does not collide). When dedupeKey is omitted, suppression is by source:type:agentId:sessionKey.
  • When a bounded target or the global store is full, the lowest-priority oldest signal is evicted; the incoming signal is rejected with queue-full if every eviction candidate has strictly higher priority.
Signals are ephemeral — not a durable queue. Plugins that need guaranteed delivery should keep their own durable state and republish a concise signal when the runtime is available. Use Agent Signals as plumbing, not product policy:
  • Put domain logic in the plugin; publish concise summaries.
  • Use context mode for passive context; heartbeat mode when the agent should consider acting soon.
  • Add dedupeKey for recurring or retried stimuli, and ttlMs for stale alerts.
  • Avoid including secrets or large payloads.

What’s next