External Store Access
Stores are not workflow-only. Any code with a StoreClient – an HTTP handler, a webhook receiver, a CLI – can read and write a store directly. This is how events get into the system: an external write wakes any workflow that is waiting on the state it changed.
Getting a handle
storeClient.store(definition, id) returns a plain async handle – no workflow, no steps.
import { SqliteStoreClient } from "@yieldstar/sqlite-runtime";
import { ConversationStore } from "./shared";
const storeClient = new SqliteStoreClient({ db, schedulerClient });
const conversation = storeClient.store(ConversationStore, "conversation:123");The constructor takes a scheduler as well as the database. When a write changes state that a suspended workflow is waiting on, the store client hands that workflow's event to the scheduler, which queues it for re-execution. See Resident-process runtime for the full wiring.
Reading
const { state, instanceId, version } = await conversation.get();instanceId is an internal UUIDv7 assigned when the store is created. It stays
the same for the lifetime of that store. Deleting and recreating the same
logical definition and ID assigns a new instance ID and resets the version to
zero.
Writing
update takes the same synchronous, pure draft-mutating updater as the workflow
API. The runtime validates the resulting state and increments the version by
one. Concurrent updates are retried against the latest state, so the updater
must be synchronous and side-effect-free.
await conversation.update((draft) => {
draft.messages.push({
id: crypto.randomUUID(),
content: "hello",
processed: false,
});
});If a workflow is suspended on store.when or store.take over s.messages, this write wakes it. That is the whole event-ingestion story: the webhook handler writes state, and the workflow that cares about that state resumes.
When a write or deletion depends on an earlier snapshot, use the conditional operations:
const snapshot = await conversation.get();
const updated = await conversation.updateFrom(snapshot, (draft) => {
draft.status = "synced";
});
const current = await conversation.get();
const deleted = await conversation.deleteFrom(current);They commit only if both instanceId and version still match. StoreClient
also exposes listStores(definition), which returns sorted live logical IDs,
and idempotent unconditional deleteStore({ definition, id }) for
administrative cleanup.
The pattern in full
// HTTP handler – external side
app.post("/conversations/:id/messages", async (req) => {
await storeClient
.store(ConversationStore, req.params.id)
.update((draft) => {
draft.messages.push(req.body);
});
return new Response("ok");
});
// Workflow – waiting side
const msg = yield* store.take(
`next:${turn}`,
(s) => s.messages.find((m) => !m.processed),
(draft, m) => {
draft.messages.find((x) => x.id === m.id)!.processed = true;
}
);External updates are not workflow steps and have no step key. If a request fails or your handler retries, read the current store and decide whether the write still needs to be applied.