State, config, and system namespaces
api.runtime.config
api.runtime.config
Current runtime config snapshot and transactional config writes. Prefer
config that was already passed into the active call path; use
current() only when the handler needs the process snapshot directly.mutateConfigFile(...) and replaceConfigFile(...) return a followUp
value, for example { mode: "restart", requiresRestart: true, reason },
which records the writer intent without taking restart control away from the
gateway.api.runtime.system
api.runtime.system
System-level utilities.
requestHeartbeatNow(...) is tracked as plugin-runtime-api-compat-aliases in the compatibility registry with a removeAfter date of 2026-10-01; use requestHeartbeat({ source, intent, reason }) in new code.The openclaw/plugin-sdk/system-event-runtime helpers resolve legacy session
aliases at the SDK boundary. Pass a resolved agentId alongside sessionKey
to api.runtime.system.enqueueSystemEvent(...) to retain the plugin runtime’s
lifecycle checks. Standalone callers can use
enqueueRoutedSystemEvent(text, { agentId, sessionKey }). Read the same owner’s
events with peekSystemEventEntries(sessionKey, agentId); this keeps global
queues separate for each agent. Calls without an explicit owner retain
configured-owner alias resolution and reject ambiguous agent selection.
Explicit owners that cannot normalize to an agent ID are rejected.runHeartbeatOnce(...) runs a single heartbeat cycle immediately, bypassing the normal coalesce timer. Delivery defaults to the configured operator DM (commands.ownerAllowFrom, then channel allowFrom); pass { heartbeat: { target: "none" } } for an internal-only run.runCommandWithTimeout(...) returns captured stdout and stderr, optional
truncation counts, code, signal, killed, termination, and
noOutputTimedOut. Timeout and no-output-timeout results report code: 124
when the child process does not provide a non-zero exit code. Non-timeout
signal exits can still return code: null, so use termination and
noOutputTimedOut to distinguish timeout reasons.api.runtime.events
api.runtime.events
Event subscriptions.
api.runtime.logging
api.runtime.logging
Logging.Generate private transport tokens with
generateSecureToken({ bytes: 32, redact: true })
from openclaw/plugin-sdk/secure-random-runtime. The object form requires at least
16 random bytes and registers the generated value for exact diagnostic redaction
before returning it. Existing numeric calls keep their ordinary ID behavior.
This grants no credential access or request authority; preserve live protocol
values and redact only at presentation boundaries.api.runtime.state
api.runtime.state
State directory resolution and SQLite-backed keyed storage.Keyed stores survive restarts and are isolated by the runtime-bound plugin id. Use
registerIfAbsent(...) for atomic dedupe claims: it returns true when the key was missing or expired and registered, or false when a live value already exists without overwriting its value, creation time, or TTL. Use observe(...) with compareAndApply(...) when a mutation depends on the current value; the comparison and mutation run in one SQLite worker transaction. Each namespace owns its maxEntries retention policy and optional TTL expiry; there is no aggregate row limit across a plugin’s namespaces. JSON values are limited to 1 MiB of UTF-8 encoded JSON. By default, a write over maxEntries sheds the oldest live rows only from that namespace. Set overflowPolicy: "reject-new" for durable ownership records that must never be evicted: new keys fail at the namespace limit, while existing keys remain updateable. Growth in a sibling cache cannot reject or evict those ownership records. Existing databases need no migration or cleanup when upgrading; their stored rows are preserved.To retain records without count-based eviction, use the async opener with retention: "retained" instead of maxEntries:OpenKeyedStoreOptions remains the bounded option type. OpenRetainedKeyedStoreOptions describes retained settings, and OpenAsyncKeyedStoreOptions is the async opener’s union. Synchronous openers accept bounded settings only.Retained stores use the existing SQLite table under an internal @retained. namespace prefix, which cannot collide with a valid caller-supplied namespace. They do not consume bounded-store row quotas. They reject maxEntries, overflowPolicy, default TTL, and per-write TTL; records remain until explicitly deleted or cleared. The per-value JSON limit still applies, and the plugin owns disk growth and deletion policy. Caller-supplied namespaces keep their existing validation and length limits.entriesInKeyRange({ keyStartInclusive, keyEndExclusive, limit, order }) reads a lexical key range, including the lower bound and excluding the upper bound. limit must be a positive safe integer; order is "asc" by default or "desc". Storage applies ordering and the limit before returning values. Encode sortable keys when native identifiers do not sort lexically. Use bounded pages rather than entries() to read a growing retained store.moveEntriesFrom({ namespace, entries: [{ sourceKey, targetKey }] }) promotes at most 10,000 rows from a bounded namespace owned by the same plugin into the receiving retained store. One transaction rereads and moves the source records without decoding or rewriting their payloads. Existing destination records win, missing source records are no-ops, and a retry after a completed move is idempotent. Live source records with TTL reject the whole operation; expired records are not revived. The returned number counts settled source rows. This operation does not create another table or require a Doctor step.These two methods remain optional in the public store type for existing adapters. A plugin using retained storage must require the host capabilities it needs; do not silently fall back to an evicting store or retry failed reads through a different path. Retained runtime handles reject operations after their owning capability closes.lookupMany(keys) is an optional keyed-store capability for at most 10,000 exact keys per call. Results have the same length and order as the input, including duplicates. Each position is a Result<T | undefined, PluginStateStoreError>: { ok: true, value } on success, including value: undefined for missing or expired keys, or { ok: false, error } for corrupt stored JSON. An empty request returns []. Keys use the same trimming and 512-byte UTF-8 limit as lookup; invalid keys or an oversized request fail with PLUGIN_STATE_INVALID_INPUT and operation lookup before reading. Database acquisition and query errors fail the whole call. Corrupt-JSON errors retain the lookup error code and operation in their per-key result. Inspect each result only when the reader reaches that position, and throw result.error if it is not ok; this lets a reader stop at an earlier missing or invalid chunk without raising a later corruption error. Each call uses one expiry cutoff and one SQLite selection in the same plugin and namespace, without creating a missing database. Separate calls, including metadata reads, do not share a snapshot; chunked formats must retain their generation, digest, and reader-lifetime checks.Current host factories provide lookupMany, but the public store types keep it optional for existing third-party adapters and declared older host versions. A plugin supporting those hosts must check the method and use its existing sequential lookup path when absent; never retry a failed bulk read through that path. Matrix, Microsoft Teams, and Voice Call retain this compatibility until their declared minimum host supplies the capability. Do not import a new helper export from an older host just to detect this method.count() returns the number of live stored rows in the runtime-bound plugin and namespace without loading or decoding their JSON values. A row expires when its expiry timestamp is at or before the call’s cutoff. Counting does not delete expired rows, create a missing database, or join the writable database lifecycle. Corrupt JSON still occupies a live row and is counted; lookup and entries retain their decoding errors. Database acquisition and query failures propagate with operation count. A count and a later write are separate operations; the write remains responsible for enforcing capacity.Current host factories provide count, but it remains optional in the public async and synchronous store types for shipped hosts and adapters through the next Plugin SDK major. Callers supporting those stores can use store.count ? await store.count() : (await store.entries()).length; synchronous callers omit await. The fallback retains the older store’s enumeration and decoding behavior. Only fall back when the method is absent, never after a failed count.openSyncKeyedStore<T>(...) remains available for callers that cannot await, with its existing synchronous return values and errors. It is deprecated through the next-plugin-sdk-major compatibility gate. See Synchronous keyed store migration.openBlobStore<TMetadata>(...) stores bounded binary payloads in shared SQLite without base64 or file sidecars. It requires per-entry, per-namespace byte, and row limits; copies byte arrays at the API boundary; and lists metadata without loading every BLOB. register(...) is an explicit upsert, including for expired keys. registerIfAbsent(...) provides collision-safe creation: an expired key remains occupied until its owner claims it with deleteExpiredKey(key) or deleteExpired(), preserving metadata needed to remove related named artifacts after the SQLite commit. Any row with a TTL is transient and excluded from backup/restore even before it expires; omit TTL for durable, restorable state. Host fuses cap each BLOB at 100 MiB, each plugin at 512 MiB of physically stored BLOBs, and each plugin at 50,000 physically stored rows, including expired rows awaiting owner cleanup. Use registerIfAbsent(...) with overflowPolicy: "reject-new" when external materializations must not be silently orphaned by replacement or eviction.Blob mutations use the shared SQLite worker and keep quota checks and changes in one transaction. lookup and entries use the retained read-only worker path. Missing stores stay absent. Ordinary unselected reads observe independently committed data; an unrelated cached native cursor can retain an older view. An explicitly selected snapshot keeps its private source through completion. Await all methods before publishing dependent artifacts or removing their storage. Shared reader admission is bounded: process inventories sequentially or with bounded concurrency, and join every started operation before reporting a batch failure or completing shutdown. Worker errors preserve PluginBlobStoreError classification, operation, path, and causal errors. Byte copying, metadata serialization, and complete result materialization still use caller memory; this is not a streaming BLOB API.openChannelIngressQueue<TPayload>(...) opens a persisted ingress queue scoped to the calling plugin, for buffering inbound events that need at-least-once processing across restarts. When stale-claim recovery uses shouldRecover, also provide shouldRecoverCorrupt if corrupt claimed payloads should be quarantined: its payload-independent claim identity lets the plugin preserve live owner and lane policy before the queue tombstones the row.Plugin-state leases were removed in 2026.8.1. Use short SQLite transactions for atomic database work and plugin-scoped keyed stores (openKeyedStore or openSyncKeyedStore) for bounded durable state.openChannelIngressDrain(...) opens the core channel-agnostic worker over that queue (or creates a queue when none is supplied). The drain owns stale-claim recovery, per-lane claim serialization, complete-at-adoption or complete-on-dispatch-return, retry/dead-letter disposition, optional pre-adoption supersede, and claim→adoption stall timeout. Wire claim ownership into reply generation with turnAdoptionLifecycle (via bindIngressLifecycleToReplyOptions from plugin-sdk/channel-outbound). Channel plugins keep accept-side enqueue, lane derivation, non-retryable classification, and any supersede authorization policy.Synchronous keyed store migration
api.runtime.state.openSyncKeyedStore and PluginStateSyncKeyedStore are deprecated
as of September 11, 2026. The existing createPluginStateSyncKeyedStore factory is
the named plugin-state-sync-keyed-store compatibility adapter. Existing methods
remain supported through the next Plugin SDK major; removal also requires a
supported external-plugin migration and explicit breaking-release approval.
Use api.runtime.state.openKeyedStore with the same namespace and options, then
await its operations. The opener itself still returns a store synchronously.
Both interfaces use the same plugin-scoped data, so no data migration is needed.
Deferred runtime code without a bound plugin API can import
createPluginStateKeyedStore from openclaw/plugin-sdk/plugin-state-store-runtime.
Pass the plugin ID and the same namespace options, then await each operation.
Keep this import lazy because the factory loads the state database runtime.
store.withCurrent before starting effects. Bind the host-provided
assertion together with any action-specific permission check:
PluginStateKeyedStore<T, 2> is an immutable binding to the same
namespace, settings, and plugin lifetime. It exposes the data-only operations;
it has no update, deleteIf, or rebinding method. The assertion stays on the
host and is checked after reads and at both transaction and final commit
admission for writes, including bounded stores. Create a separate view for each
action; do not keep one caller’s authority on a shared service. The legacy
PluginStateKeyedStore<T> keeps this capability optional for older hosts and
adapters. An action requiring it must refuse when it is absent.
observe and a comparison conflict return observations without committing the
requested mutation; they also require current authority when returning that data.
A refusal before the commit grant rolls back the mutation. Once commit is
authorized, later revocation does not turn the settled write into a refusal.
Recheck authority before the next external effect, and preserve the recorded
result; never retry a committed or unknown write to compensate for revocation.
The async store’s update updater and deleteIf predicate are deprecated
compatibility methods. They still run synchronously on the main thread inside
the transaction containing the authoritative read and mutation, and remain
supported through the next Plugin SDK major.
Finish asynchronous planning before calling these methods; do not make their
callbacks async or replace atomic operations with separate lookups and writes.
Returning undefined from an updater leaves the entry unchanged. update,
deleteIf, lookupMany, and count remain optional in public store types, so preserve
capability checks for supported older hosts and third-party adapters.
For new atomic mutations, use the optional observe and compareAndApply
methods. observe(key) prepares a mutation through canonical writable database
admission and may create or open state. It returns { value, comparison }; use
lookup for a plain, noncreating read. No transaction remains open while the
caller prepares the next value.
compareAndApply(key, comparison, intent) compares the current live row before
changing it in the same worker-owned transaction. The opaque comparison binds
the actual database, plugin, namespace, key, stored JSON bytes, creation time,
and expiry. It compares content and metadata; it is not an incarnation token or
permission to act. Another store or key rejects the comparison with
PLUGIN_STATE_INVALID_INPUT.
The intent is explicit:
{ operation: "update", action: "set", value, ttlMs? }writes a defined value and refreshes its creation time and TTL, even when the value is unchanged.{ operation: "update", action: "keep" }leaves the entry unchanged while retaining writable admission and the existing namespace expiry sweep.{ operation: "delete", action: "delete" }removes a matching live entry.{ operation: "delete", action: "keep" }retains writable admission without an expiry sweep or entry mutation.
{ status: "applied" }, { status: "unchanged" }, or
{ status: "conflict", current }. A conflict does not change plugin-state rows
and supplies a fresh observation. An entry that expires after observation
conflicts; missing and expired entries otherwise share logical-absence semantics.
Existing quotas, eviction order, validation, and store errors still apply.
On an explicit conflict, a plugin may recompute a named pure decision from
current.value and try again. Prepare clocks, randomness, and external effects
outside that decision. Never retry transport failures, unknown outcomes, or
arbitrary callbacks. Check both optional methods before using this capability;
there is no safe fallback consisting of a separate lookup and unconditional write.
registerIfAbsent and the optional deleteIfEqual(key, expected) operation use
the shared-state SQLite worker. deleteIfEqual accepts a string, finite number,
boolean, or null, and compares it with the decoded live value in the same
transaction as deletion. Missing or expired entries return false; malformed
stored JSON remains a typed store error. These operations share existing data,
limits, and expiry rules with the legacy synchronous store.
register, lookup, lookupMany, consume, delete, entries, count, and clear
also execute SQLite in the same worker. Reads preserve missing-store behavior
without creating a database. lookupMany returns one result per input key,
including duplicates and per-key corrupt-value errors. consume reads and
deletes atomically; a decode failure rolls back the deletion. Store creation,
input validation, and JSON serialization remain on the calling thread.
Callback-based update and deleteIf retain the native synchronous transaction;
do not replace either with a separate lookup and write. Worker errors retain PluginStateStoreError codes, operation, and path. Canonical
state errors use their existing codec; other native causes retain bounded causal
messages and error codes. Arbitrary custom properties and original stacks do not
cross the worker boundary.
Discord and Slack use scalar conditional deletion when relinquishing a presence
cooldown. On older hosts without that optional capability, they leave it to expire
instead of risking deletion of a newer reservation.
FaceTime persists pending dial snapshots in invocation order and uses worker
comparisons to clear only the matching dial. Helper dispatch waits for durable
intent, and shutdown joins accepted persistence. Its supported 2026.9.4 hosts
without comparisons retain atomic deleteIf cleanup; a failed worker operation
never selects that compatibility path. The namespace, stored records, and
retention remain unchanged, so this cutover requires no data migration.
This deprecation adds editor annotations, documentation, and compatibility
inventory metadata. It adds no runtime warning and changes no trust eligibility:
the runtime openers remain limited to bundled plugins and trusted official
installations. Runtime warnings should wait for an actionable supported upgrade.
Only the operations identified above execute on the worker. Callback execution
is unchanged during this migration.
Per-agent SQLite writes
Bundled and official plugins that already use the privatesqlite-runtime
facade can import withOpenClawAgentDatabaseWrite from
openclaw/plugin-sdk/sqlite-runtime. This remains an internal runtime facade,
not a typed public SDK entrypoint for third-party plugins.
Call it from an asynchronous producer before entering synchronous SQLite. It
shares the agent database’s in-process write admission with session writers and
off-thread reclamation, leaving the Gateway thread available to authorize a
reclamation commit.
Asynchronous AgentSession message, model, compaction, and tree operations use
this admission for their transcript writes. Embedded prompt preparation, replay
repair, and tool-result cleanup await their writes before publishing dependent
results or disposing their resources. Model-selection hooks run after write
admission releases. SessionManager appendModelChange and
appendThinkingLevelChange return promises for their committed entry IDs;
AgentSession and extension setThinkingLevel return Promise<void>. Await these
operations before using the resulting model or thinking state. Other synchronous
SessionManager operations still need an appropriate caller-owned write boundary.
SessionManager.open, openBounded, and setSessionTarget capture storePath
as an absolute lexical locator before reading the transcript or invoking
onTruncated. Relative locators resolve against the process working directory
at entry; getSessionTarget() returns that captured locator. Later working
directory changes leave the manager bound to its original store. Existing
sessions.json and custom-store routing and symlink spelling are preserved.
File-backed model and thinking transcript writes execute through the canonical
agent database worker. Queued extension actions retain their original runtime
and session authority through transaction and commit admission. Session opening,
final model-context validation, and incognito transcript persistence still use
their native owners; an asynchronous method does not imply that every storage
operation in the enclosing session flow runs off-thread.
Committed metadata updates the bound session’s model or thinking state alongside
transcript-view adoption, before asynchronous cleanup. Settings setters retain
their existing persistence queue. If view reconstruction, local publication, or
a dependent thinking change fails after the append commits, the error preserves
the committed entry and prevents model fallback from replaying it. A failed view
reconstruction makes the existing manager refuse further transcript access;
discard it and reopen through the session owner after resolving the read failure.
Retrying the append would duplicate a write that already committed.
The signature is withOpenClawAgentDatabaseWrite(options, operation, expectedDatabase?).
options uses the existing agent database options, including the required
agentId and optional concrete path. The synchronous operation receives the
OpenClawAgentDatabase; the returned promise resolves to its result after the
operation settles. Without expectedDatabase, the helper also owns asynchronous
database-open admission. The helper captures the environment, selected state
directory, and database path before waiting, so changing the working directory does not retarget
a queued write. Opening or borrowing a handle alone does not admit a write.
For an already borrowed handle, pass its exact DatabaseSync as the third
argument. After waiting, the helper rejects a closed or replaced handle rather
than opening a replacement on its behalf. Keep the original borrow alive until
the operation settles. The caller still owns transactions and authorization.
For large native publications, openOpenClawAgentSqliteWorkerStore(options, borrowedDb, { moduleUrl, input })
retains the original borrowed handle and physical identity. Its
run(operation, assertCurrent) joins the existing agent writer queue and borrows
the canonical agent executor for the complete operation. The module exports
bindSqliteWorkerBackend(input, { databasePath, database, admit }); it uses the
supplied connection and closes only its own temporary state. It must not open or
close the agent database. The operation receives only the bound backend’s
execute method; finish it before calling close(). Client close revokes new work,
drains its accepted operations, and releases its original borrow. The canonical
executor owns the native connection, lease, idle reuse, and final close.
A backend used with this owner requests transaction admission after BEGIN and
commit admission immediately before COMMIT through the supplied admit callback.
The host checks the canonical connection and current caller authority at
both points without waiting synchronously for the native transaction. An accepted
commit grant orders the commit before later revocation; an earlier refusal rolls
back. Callers must preserve committed or unknown outcomes and never replay them.
Private file owners can use runSqliteWorkerStoreWrite with their own admission
and lifetime; it does not supply the shared agent queue or lease.
Worker backends can load module prerequisites asynchronously in prepare(command).
Preparation carries captured state/runtime facts and performs no native work.
After it settles, execute(command) enters fresh synchronous authority scopes;
connection-bound execution revalidates authority before native work. Extension
loading, transactions, and domain callbacks remain synchronous. Agent connection policy, including TEMP
storage, belongs to the canonical connection owner and cannot be reset when a
publication binds.
Backends whose failure handling can leave an unusable native connection implement
synchronous assertSettled(). The broker calls it after a command returns or
throws. A failed assertion retires the Worker and waits for native exit before
releasing operation admission. Use assertTransactionUsable(db) to detect the
transaction owner’s retained failure, and reject any surviving open transaction.
Ordinary failures that rolled back safely can still return their domain result.
For example, given an existing borrowedDb, a live-owner assertCurrent() check,
and synchronous applyPreparedChanges(db):
runSqliteImmediateTransaction(db, prepare, options, admit) accepts the same
owner’s admission callback. prepare runs before admission and returns a
synchronous transaction callback. Pass (write) => withOpenClawAgentDatabaseWrite(databaseOptions, write, borrowedDb) as admit;
do not place asynchronous preparation inside the admitted callback. The helper
rechecks transaction state after waiting and never repeats a callback that
already entered its transaction.
withOpenClawAgentDatabaseWrite does not start a transaction, grant an authority
lease, or coordinate unrelated processes. Raw SQLite calls outside admission bypass it, and existing
synchronous APIs do not become asynchronous automatically. A rejected stale-owner
write must return to its lifecycle owner for recovery, not retry with a replacement
handle. For storage design and migration requirements, see
Database schemas.