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:
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

Available helpers

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:
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:
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