Crux
GuidesDurable ExecutionDurable Sessions

Durable Sessions

Own a long-lived Agent or Flow conversation with ordered durable turns, Signal ingress, streams, and lifecycle controls.

A durable Session is a keyed, restart-safe owner for one Agent or one exported Flow. It is not one endless model request, a transport, a Channel, or a bare Flow handle. Create or reopen the Session, accept ordered inputs (and optional Signal subscriptions), and join exact results when the Runtime worker finishes each activation.

Crux also uses the word "session" for Safety sessions, skill activation, and observability grouping. This guide is only about the durable Session API: session() / getSession().

When to use one

Use a durable Session when:

  • one Agent or Flow must own one business conversation across requests or process restarts;
  • acceptance must be durable even if the worker has not finished yet;
  • concurrent messages should keep server-assigned order;
  • Signal fan-out should feed Session-owned ingress or Flow waiters with restart-safe identity;
  • operators need payload-safe status, inspection, streams, and statistics without reading prompts or tool arguments.

Do not use one for a single request-scoped generation or a fire-and-forget exported Flow occurrence without a stable conversation key (Application Work). For typed process-local events without Session ownership, start with Signals.

Smallest working Flow Session

Export a Flow, generate the Runtime program, bind a Work host, create the Session, send, and join the result:

import { createWorkHost, session } from "@use-crux/core";
import { flow } from "@use-crux/core/flow";
import { node } from "@use-crux/core/runtime";
import { runtimeProgram } from "./.crux/generated/runtime/program";

export const checkout = flow(
  "checkout",
  async (scope, input: { orderId: string }) => ({
    ok: true as const,
    orderId: input.orderId,
  }),
);

const host = createWorkHost({
  runtime: node({ namespace: "app" }),
  program: runtimeProgram,
});

const order = await host.run(() =>
  session(checkout, { key: "order-42" }),
);
const turn = await order.send({ orderId: "order-42" });
const output = await turn.result(); // { ok: true, orderId: string }

send() resolves when the input is durably accepted. It does not wait for execution. result() waits for the canonical Work occurrence that owns the activation.

Process-local node() is enough for demos and tests. Production durability needs a durable Runtime store and a Session-owned Thread RecordStore on the same database; see recipes.

Smallest working Agent Session

Agent Sessions need a bound GenerationModel:

import { createWorkHost, prompt, session } from "@use-crux/core";
import { agent } from "@use-crux/core/agent";
import { node } from "@use-crux/core/runtime";
import { aiSdk } from "@use-crux/ai";
import { z } from "zod";
import { runtimeProgram } from "./.crux/generated/runtime/program";

const supportModel = aiSdk(nativeModel("nebula-text-v2"));

export const support = agent({
  id: "support",
  model: supportModel,
  prompt: prompt({
    input: z.object({ message: z.string() }),
    output: z.object({ reply: z.string() }),
    system: "Reply to the customer.",
    prompt: ({ input }) => input.message,
  }),
});

const host = createWorkHost({
  runtime: node({ namespace: "app" }),
  program: runtimeProgram,
});

const conversation = await host.run(() =>
  session(support, { key: "customer-42" }),
);
const turn = await conversation.send({ message: "Hello" });
const output = await turn.result(); // { reply: string }

Identity and create / get

A Session key is caller-controlled and required. Lookup identity is the hash of Runtime namespace plus key. The public Session id and automatic Thread id also include the stable target id (Agent id or Flow name), so two targets cannot silently share one key.

import { getSession, session } from "@use-crux/core";

const created = await host.run(() =>
  session(support, { key: "customer-42" }),
);
const reopened = await host.run(() => getSession(support, "customer-42"));
// created.id === reopened.id
CallBehavior
session(target, { key })Create or reopen. Returns only after the Session-owned Thread owner is ready.
getSession(target, key)Retrieve without creating. Fails if missing.

Compatible concurrent session() calls with the same target and key return the same Session. The same key with a different target throws SESSION_IDENTITY_CONFLICT. Missing keyed retrieval throws SESSION_NOT_FOUND. Deleted keys remain tombstoned and reject silent recreation.

What this guide covers next

TopicPage
Signal ingress, streams, lifecycle, fork, stats, recipesActivation, Signals, and lifecycle
Copy-paste deployment patternsRecipes
Exact types and errorsAPI reference

On this page