Skip to main content

State & Scopes

State in AI applications is messy. Conversation history, user preferences, shared configuration, intermediate processing data — all at different lifetimes, all needing different isolation guarantees. flow-state.dev gives you four scoped levels with typed operations.

This page walks through the scopes and the basic patterns you'll use every day. For the full operation reference (CAS semantics, error handling, every helper signature), see State Operations.

The four scopes​

State is organized into four scopes, each its own record with its own key:

ScopeQuestion it answersLifetime
RequestWhat does this single action execution need right now?One action execution
SessionWhat does this conversation need to remember?Across requests in a conversation
UserWhat does this user need across their conversations in one organization?Across sessions for a user, per organization
OrgWhat does the team need to share?Across sessions in an org
The four state scopes
Request, session, user and org state are four separate records, none inside another. Request state belongs to one action run and the next request cannot see it. Session state belongs to one conversation: its state, items, metadata and journal, and the resources an action declares, keyed by the session id and by the tenant when one is sent. User state follows one user across conversations and flows inside one organization, keyed by the user and the organization; with isolateUserState it is one record per flow copy instead. Org state is shared by everyone in the organization, keyed by the org id, or one record per flow copy with isolateOrgState. A session is bound to one organization when it is created; it names its user and org but holds neither one's state. A tenant id splits sessions only. Blocks read and write all four; the browser sees only the fields a scope's client config exposes4 SEPARATE RECORDS · NONE SITS INSIDE ANOTHERSCOPE 01Requestone action runctx.requestScratch space betweenthe blocks of one runKEYthe requestThe next requestcannot see it.SCOPE 02Sessionone conversationctx.sessionIts state, items,metadata and journal,and the resources anaction declaresKEYthe session id, andthe tenant when sentSCOPE 03Userone personctx.userFollows the personacross conversationsand flows in oneorganizationKEYthe userId and orgIdisolateUserStateone record perflow copy insteadSCOPE 04Orgeveryone in the organizationctx.org?Settings and knowledgethe whole team sharesKEYthe orgIdisolateOrgStateone record perflow copy insteadNAMES · HOLDS NO COPY01A session is bound to one organization when it is created. It points at its user and org; it does not copy theirstate.02A tenant id splits sessions only. User and org records are the same across tenants.03Blocks read and write all four. The browser sees only the fields a scope's client config exposes.LASTS ONE ACTION RUNOUTLIVES THE REQUESTNAMES (A REFERENCE)

A session names the user and organization it belongs to. It doesn't contain their state: each scope is read and written on its own record.

Most of your state lives at the session level. The other three matter, but they show up after you've shipped your first conversation. Start with session.

Not everything on the context is state. Read-only instance config arrives separately as ctx.settings — see Engine setup → Settings.

Session: the primary scope​

A session is one conversation. When a client sends a request to a flow with a sessionId, the framework loads that session's state, runs the action, and persists any changes. The next request with the same sessionId picks up where the last one left off.

A block declares the session-state fields it touches with sessionStateSchema. Inside execute, ctx.session is the read/write handle:

const tracker = handler({
name: "tracker",
sessionStateSchema: z.object({
mode: z.enum(["chat", "agent"]).default("chat"),
messageCount: z.number().default(0),
}),
execute: async (_input, ctx) => {
// Read — typed from the schema
const mode = ctx.session.state.mode;

// Merge fields into existing state
await ctx.session.patchState({ mode: "agent" });

// Atomic numeric increment
await ctx.session.incState({ messageCount: 1 });
},
});

ctx.session.state is fully typed — the framework infers the type from your Zod schema. Reads, writes, and increments are all checked at compile time. See Type System for how this carries through blocks, sequencers, and flows.

These three operations (patchState, incState, and the record helpers covered next) cover most of what you'll write. There are more — setState for full replacement, pushState for arrays, atomicState for read-modify-write — but you don't need them on day one.

Record helpers​

Sessions often hold maps of records — chat threads, saved items, anything keyed by ID. setStateRecord and deleteStateRecord work with those without you having to spread the whole map yourself:

await ctx.session.setStateRecord("byId", "doc-1", {
title: "Design Doc",
updatedAt: Date.now(),
});

await ctx.session.deleteStateRecord("byId", "doc-1");

Each one writes a single key rather than the whole map, so two runs updating different keys don't overwrite each other. Two runs updating the same key do — the second write replaces the first, and both calls report success. Concurrent writes has the rule per call, including the cases where deleteStateRecord reports that it did nothing.

Schema bubbling​

Here's the part that makes blocks portable: you don't have to declare every state field at the flow level. When a flow is constructed, state declarations on individual blocks bubble up and merge into the flow's combined schema.

const counter = handler({
name: "counter",
sessionStateSchema: z.object({ messageCount: z.number().default(0) }),
execute: async (_input, ctx) => {
await ctx.session.incState({ messageCount: 1 });
},
});

const modeSwitch = handler({
name: "mode-switch",
sessionStateSchema: z.object({ mode: z.enum(["chat", "agent"]).default("chat") }),
execute: async (_input, ctx) => {
await ctx.session.patchState({ mode: "agent" });
},
});

When these blocks are composed into a flow, their state declarations are collected and merged into the type of ctx.session.state. The flow's session state is typed { messageCount: number, mode: "chat" | "agent" } without you repeating those fields in a flow-level stateSchema.

Block declarations shape the types. They don't fill in defaults when a run reads stored state; only the flow's own stateSchema does that (see Creating sessions). Until something writes mode, a block that reads ctx.session.state.mode gets undefined unless the flow declares the field too.

You can still define a flow-level schema if you want one place to see everything:

defineFlow({
kind: "my-app",
session: {
stateSchema: z.object({
messageCount: z.number().default(0),
mode: z.enum(["chat", "agent"]).default("chat"),
}),
},
});

The flow-level schema needs the fields a block reads before anything writes them, so their defaults apply, and the fields the flow itself references (like in a client.expose list or a client.derived compute function).

Why bubbling matters​

The point is blocks shouldn't depend on flows. A counter block that needs messageCount declares it on itself. A mode-switching block declares mode. Neither needs to know about the other.

// These blocks work in any flow — they bring their own state requirements
import { counter } from "@shared/blocks";
import { modeSwitch } from "@shared/blocks";

const pipeline = sequencer({ name: "chat" })
.tap(counter) // bubbles up { messageCount }
.tap(modeSwitch) // bubbles up { mode }
.step(agent);

If two blocks declare the same field with incompatible types, the framework catches it as a type error during flow construction. Schema conflicts surface at build time, not runtime.

For shared blocks used across codebases, namespace your fields (e.g., analytics_eventCount instead of count) to avoid collisions. Within a single codebase, consistent naming is usually enough.

Resource declarations bubble too — see Blocks.

Sessions in depth​

Sessions are the richest scope. Beyond state operations (which all scopes share), sessions provide:

Items — the accumulated output of all requests in the conversation, with audience-specific views:

const allItems = ctx.session.items.all();
const clientItems = ctx.session.items.client();
const llmMessages = await ctx.session.items.history({ limit: { tokens: 20_000 } });

Limits on items.history() operate on conversational turns rather than individual protocol messages — see Conversation history windowing. By default it also includes what the current request has produced so far; pass includeInFlight: false for earlier turns only.

History is windowed by default. Each request loads only the most recent session.historyWindow.turns completed turns (default 50), so per-turn cost does not grow with session length. items.all() and items.client() reflect that same loaded window, and a per-call history({ limit }) narrows within it. The complete session log is never loaded into the execution context on the hot path; read it through the GET /sessions/:id/state endpoint when you need the full history. See Flow-level history bounds.

Metadata — first-class title, description, and tags fields that live outside workflow state:

const { title, description, tags } = ctx.session.metadata;

await ctx.session.setMetadata({
title: "Sprint planning",
description: "Q2 kickoff",
tags: ["planning"],
});

Journal — an append-only log for session-level notes and events:

await ctx.session.appendJournal({
text: "User switched to agent mode",
source: "mode-router",
});

const recent = await ctx.session.getJournal({ limit: 10 });

Resources — named typed containers for structured data and rich content. A request loads only the resources its dispatched action and blocks declare, not every resource in the scope. See Resources and When resources load.

Creating sessions​

A new session typically starts via fsd.flow(address).createSession({...}) on the client, which returns a stable sess_<id> you reuse on every subsequent action call. See Client Overview.

If you call an action without a sessionId, the framework generates a fallback ID (prefix ephemeral_<ts>_<rand>) and persists the session record like any other. The action route doesn't return that generated ID to the client, so the session is effectively orphaned — useful for one-shot internal callers and tests, but not a way to start "real" conversations. For production conversational flows, always create the session first and pass the ID through.

A caller can pass initial state when it creates a session. It is parsed through the flow's session stateSchema, so missing fields take their defaults. A state that doesn't fit the schema is kept as sent, and your actions validate it when they run, unless the flow binds its sessions, meaning it declares a readonly field or a createCheck.

Defaults also apply every time a run reads stored state. When a session, user or org record loads, each field of the flow's stateSchema (session, user or org) that the record doesn't hold reads as its default, so ctx.user.state.count is 0 on a brand-new user and on one stored before you added the field. Only missing values are filled in. A value already stored is read as stored, and keys the schema doesn't declare are kept, so another flow's fields on a shared user or org record survive your reads and writes.

Initial state suits preferences and drafts. On its own it is the wrong place for a value that grants anything, because the caller wrote it; a createCheck that confirms the value (below) changes that.

For a field your flow changes as it runs, and that a caller must never set, list it in session.serverOwned:

session: {
stateSchema: z.object({ reviewers: z.array(z.string()).default([]) }),
serverOwned: ["reviewers"],
},

A create that sets reviewers in its state is refused with a 400 whose body names the field ({ error, field: "reviewers" }), and nothing is written. Blocks in your flow write it like any other session state.

When a session exists to work on one particular thing, such as one project, put that thing in the session's stateSchema and declare it with zod's .readonly(). A readonly field is set when the session is created and never changes. Blocks read it like any other session state, and callers can list sessions by it.

If your flow binds sessions this way (a readonly field or a create check), the starting state must match the schema or the create is refused, with a 400 that names the field ({ error, field }) and nothing written.

Most flows need only the schema. When a rule depends on who is creating the session, or on what exists in the store, such as "this project is yours or shared with you", also declare session.createCheck. It runs after the schema has parsed the initial state, receives that state and the caller's identity, and accepts or refuses the create. The example below walks through both.

Example: one session per project​

Say your app keeps projects, and every assistant session works on exactly one of them. A session should name a project, keep it for its whole life, and only open on a project the user owns or has been given.

1. Keep the projects in a collection. A resource collection with scope: "org" holds one set of rows for the whole organization, so a project can be shared between its users. Here a small projects flow writes to it:

// flows/projects.ts
import { defineFlow, defineResourceCollection, handler } from "@flow-state-dev/core";
import { z } from "zod";

// One row per project, shared across the organization.
export const projects = defineResourceCollection({
pattern: "projects/*",
scope: "org",
stateSchema: z.object({
name: z.string(),
owner: z.string(),
sharedWith: z.array(z.string()).default([]),
}),
});

const createProjectInput = z.object({
slug: z.string(),
name: z.string(),
sharedWith: z.array(z.string()).default([]),
});

const createProject = handler({
name: "create-project",
inputSchema: createProjectInput,
resources: { projects },
execute: async (input, ctx) => {
await ctx.resources.projects.create(input.slug, {
name: input.name,
owner: ctx.user.identity.userId,
sharedWith: input.sharedWith,
});
return { slug: input.slug };
},
});

export const projectsFlow = defineFlow({
kind: "projects",
resources: { projects },
actions: {
create: { inputSchema: createProjectInput, block: createProject },
},
})();

After user_1 runs create with { slug: "q3-launch", name: "Q3 launch", sharedWith: ["user_2"] }, the organization holds the row projects/q3-launch, owned by user_1 and shared with user_2.

2. Declare the session's state. On the assistant flow, the stateSchema says every session names a project:

// flows/project-assistant.ts
import { defineFlow } from "@flow-state-dev/core";
import { z } from "zod";
import { loadProject } from "./load-project"; // defined in step 5
import { projects } from "./projects";

export const projectAssistant = defineFlow({
kind: "project-assistant",
resources: { projects },
session: {
stateSchema: z.object({ projectId: z.string().readonly() }),
},
actions: {
loadProject: { inputSchema: z.object({}), block: loadProject },
},
})();

The stateSchema line does the shape validation. .readonly() makes projectId fixed: it is set when the session is created and never changes. It also makes this a flow that binds its sessions, so the starting state must match the schema. projectId is required and has no default, so a create without a string projectId is refused with a 400 that names the field, and nothing is written. Many flows stop here.

3. Add a check for the rule that depends on the caller. The schema can't know whether q3-launch exists, or whether this user may open it. That depends on who is asking and on what is stored, which is what session.createCheck is for:

session: {
stateSchema: z.object({ projectId: z.string().readonly() }),
createCheck: async ({ state, principal, readCollectionItem }) => {
// The schema has already parsed the state, so this is a string.
const projectId = state.projectId as string;
const project = await readCollectionItem("projects", projectId);
const mayOpen =
project !== undefined &&
(project.owner === principal.userId ||
(project.sharedWith as string[]).includes(principal.userId));
if (!mayOpen) {
return { ok: false, status: 404, message: `No project "${projectId}".` };
}
return { ok: true };
},
},

The check gets one object:

FieldWhat it is
stateThe session's initial state, after the schema parsed it. A state the schema refuses never reaches the check.
principalThe caller: { userId, orgId, tenantId? }.
readCollectionItem(ref, topic)Reads one resource of a user- or org-scoped collection this flow declares. ref is the key in the flow's resources ("projects"). topic is the resource's key in the collection: "q3-launch" for projects/q3-launch, or the parameters for a pattern like [team]/info ({ team: "eng" }). Resolves the resource's state, or undefined when there is no such resource. Throws if ref isn't a user- or org-scoped collection of this flow.
sessionId, flow, viaThe id being created, the flow ({ kind, id }), and which path is creating it: "create", "action", "webhook", "cli" or "dispatch".

readCollectionItem reads only the caller's own scope: their own resources in a user-scoped collection, their own organization's resources in an org-scoped one. The check above answers "not yours" with the same 404 as "doesn't exist", so it doesn't reveal which projects exist.

Return { ok: true } to create the session, or { ok: false, message, status } to refuse it. status can be 400, 403 or 404, and defaults to 400.

The check is only as strong as the caller identity it receives. With authentication set up, principal is the authenticated user. Without it, principal.userId is whatever userId the request sends.

Register both flows with your server as usual (see Engine setup).

4. Create a session for a project. Pass the project's slug in the session's initial state:

import { createClient } from "@flow-state-dev/client";

const fsd = createClient({ userId: "user_2", baseUrl: "http://localhost:3000" });

const session = await fsd.flow("project-assistant").createSession({
state: { projectId: "q3-launch" },
});

Over HTTP, the same create is:

POST /api/flows/project-assistant/sessions
Content-Type: application/json

{ "userId": "user_2", "state": { "projectId": "q3-launch" } }

It answers 201 with { "session": { "id": "sess_...", ... } }.

In React, useFlow's createSession and autoCreateSession send no initial state, so this flow refuses them. Create the session with the client as above, then make it the hook's active session with flow.selectSession(session.id).

5. Read the field in a block.

// flows/load-project.ts
import { handler } from "@flow-state-dev/core";
import { z } from "zod";
import { projects } from "./projects";

export const loadProject = handler({
name: "load-project",
inputSchema: z.object({}),
sessionStateSchema: z.object({ projectId: z.string().readonly() }),
resources: { projects },
execute: async (_input, ctx) => {
const projectId = ctx.session.state.projectId;
const project = await ctx.resources.projects.get(projectId);
return { projectId, name: project.state.name };
},
});

Run loadProject in the session from step 4 and it returns { projectId: "q3-launch", name: "Q3 launch" }. Every turn in that session sees the same value. A block that writes a different projectId fails its run with an error naming the field, and the session keeps the value it was created with. The flow's session.stateSchema is what makes the field readonly; the block's sessionStateSchema only types ctx.session.state.

6. List a project's sessions.

const forProject = await fsd.sessions.list({
flowKind: "project-assistant",
state: { projectId: "q3-launch" },
});

Over HTTP: GET /api/flows/sessions?flowKind=project-assistant&userId=user_2&state.projectId=q3-launch. The filter narrows the listing the caller already gets, so another user's sessions on the same project never show up.

A state filter needs flowKind (or flowId), takes string values matched exactly, and only names readonly fields. A filter on any other field is refused with a 400.

7. Handle a refusal. When the create is refused, no session is written. The create answers with the status and a body with error, plus field when the schema refused it, and the client throws ClientHttpError:

import { ClientHttpError, createClient } from "@flow-state-dev/client";

const asUser3 = createClient({ userId: "user_3", baseUrl: "http://localhost:3000" });

try {
await asUser3.flow("project-assistant").createSession({
state: { projectId: "q3-launch" },
});
} catch (error) {
if (error instanceof ClientHttpError) {
error.status; // 404
error.body; // { error: 'No project "q3-launch".' }
}
}

With the flow above:

The create's stateRefused byStatusBody
A project the user owns or was given201{ session: { ... } }
A project that doesn't exist, or one the user wasn't givencreateCheck404{ error: 'No project "q3-launch".' }
No projectId, or one that isn't a stringstateSchema400{ error, field: "projectId" }

An action sent without a session id, or to a session id that doesn't exist yet, would start a session with no projectId, so it gets that 400 and nothing is written. So does a webhook delivery to a new session id. Create the session first, as in step 4.

Where else this runs. The schema and the check run on every path that creates a session of this flow: a dispatcher() starting a child session with session: { key, state }, and fsdev run project-assistant loadProject --seed-session '{"projectId":"q3-launch"}' when it starts a new session. A turn on an existing session runs neither.

The client block: exposing state safely​

Raw state never reaches the client. Session, user, and org each take a client block that declares the slice of state that crosses to the browser. Anything not in client stays on the server.

The block has two halves:

  • expose — top-level state field names that pass through verbatim. Use it for primitives that are already client-shaped.
  • derived — named projections computed from { state, resources }. Use it when the client wants a different shape (e.g. mapping a structured resource into an ID + title list).
session: {
stateSchema: z.object({
messageCount: z.number().default(0),
}),
client: {
expose: ["messageCount"],
derived: {
artifactsList: (ctx) => {
const artifacts = ctx.resources.artifacts?.state;
return artifacts?.order.map(id => ({
id,
title: artifacts.byId[id]?.title ?? "Untitled",
})) ?? [];
},
},
},
}

On the client, read these via useClientData:

const data = useClientData(session, {
session: ["artifactsList", "messageCount"],
user: ["preferences"],
});

The values land at snapshot.clientData.<scope>.<name>. useClientData reads that object.

Internal state — intermediate processing, raw resource contents, block-private fields — stays on the server. The privacy contract is the default: a scope without a client block exposes nothing. Adding a new state field doesn't silently leak to the browser; you have to add it to expose or derived first.

expose and derived share a namespace. A name in both throws at defineFlow. expose names must be top-level keys on that scope's stateSchema.

During streaming, state_change and resource_change events signal that the client view may be stale. The client refetches the authoritative snapshot on request.completed.

Mutations to session, user, org, and request state all emit state_change items on the wire, in the same shape block-instance and sequencer target state emit, so React's useClientData can reflect mid-stream patches without waiting for terminal status. See useClientData. Resources have the same option: one declaring client: { live: true } streams its projected delta the same way (see Resources: client access — Live updates), so apps don't need to mirror resource status onto session state.

A state_change item reaches only the view that sent the request. User and org state are shared beyond it: the same person in another tab, or everyone in the organization. A view created with useSession(id, { live: true }) hears those changes too, from any session or from a run with no browser attached, and reads its state again without its item history. A user's state reaches only that user's views. See Hearing requests you didn't send.

This mirrors how resources work: a resource without a client config is invisible to clients (see Resources: client access). One mental model — client everywhere — instead of two.

The other three scopes​

You'll reach for these less often than session, but each has a specific job.

Request is scratch space for one run: intermediate results, retry counters, temporary flags.

requestStateSchema: z.object({ retryCount: z.number().default(0) })

Use request state when you explicitly don't want data to accumulate in the session. The rule of thumb: if the next request might care, use session.

User follows a person from conversation to conversation: preferences, accumulated knowledge, personal collections.

userStateSchema: z.object({
preferences: z.object({
responseStyle: z.enum(["concise", "detailed"]).default("detailed"),
}).default({}),
})

User data belongs to a user inside one organization. Alice in Acme and Alice in Globex have two user records, and two copies of every user-scoped resource, so nothing she saves while working for one organization shows up in the other. Inside one organization, every flow on the server shares her one record, so each flow's user state schema is compared at startup. Incompatible declarations throw CrossFlowSchemaConflictError from FlowRegistry.register before any data can be corrupted. Flow Isolation gives each flow copy its own record instead, still inside the organization.

Org is shared by the whole team: configuration, knowledge bases, settings an admin controls for everyone.

orgStateSchema: z.object({
config: z.object({
maxTokenBudget: z.number().default(100_000),
}).default({}),
})

Org state is shared across flows the same way and gets the same startup check. Read it with ctx.org?.state.config: ctx.org is optional in the types, but every request runs in an organization, so it is there on every execution (where that organization comes from). A session is tied to the organization it was created in, and a request against it that resolves to a different organization throws OrgBindingMismatchError.

For the full operation reference and CAS semantics that apply to all four scopes, see State Operations. For how userId and orgId flow into a request — including who's responsible for verifying them — see Authentication.

Multi-tenant isolation​

If you run one deployment for several customers (your "tenants"), you can isolate their data at the store layer without writing your own filtering. Send a tenant id on each request through an HTTP header — x-tenant-id by default, configurable with createFlowApiRouter({ tenantIdHeader }). Set it from your gateway, or from your client's fetch wrapper, the same way you'd attach an auth header.

When a tenant id is present, the framework namespaces the session storage key. Two tenants that both use session id chat-1 get two separate session records, separate session state, and separate session-scoped resources. Cross-turn history and request listing are scoped to the tenant too, so one tenant never sees another's turns.

# Two tenants, same session id, two independent sessions:
curl -H "x-tenant-id: acme" ... -d '{"sessionId":"chat-1", ...}'
curl -H "x-tenant-id: globex" ... -d '{"sessionId":"chat-1", ...}'

What stays shared, on purpose: user and org scopes. Org-level policy and quotas, and a user's preferences, are usually meant to apply across tenants, so they key on the user and organization, or the organization alone, and not on the tenant. If you need those isolated per tenant, encode the tenant into the id you pass.

You read the tenant in a block the same way as any identity field: ctx.session.identity.tenantId. The session id you get back (ctx.session.identity.id, API responses) is always the bare id you sent — the tenant prefix is an internal storage detail.

Apps that never send the header do nothing: with no tenant id, a session keys on its own id alone. Persistence covers the storage side, including what a persistent adapter does to an existing database.

State tied to a run​

The four scopes are tied to identity. Blocks can also hold state tied to execution — a generator remembering what it's loaded so far, a sequencer counting its own loop passes — that lives and dies with one run. See Block State for that primitive.

When to declare state at the flow level​

In practice, most state declarations live on blocks, not flows. A counter block declares messageCount, a mode-switcher declares mode, and the flow picks them up by bubbling. You don't need to repeat them on defineFlow.

Reach for flow-level session.stateSchema (or user, org) when:

  • You write a client.derived compute function that reads the field — the compute function lives on the flow config, so the field has to be visible there.
  • A block reads the field before anything writes it, and needs its default. Defaults apply on read only from the flow's schema.
  • You want one place to see the canonical schema for code review or onboarding.
  • A field doesn't belong to any one block (rare).

A typical flow ends up looking closer to this:

const myFlow = defineFlow({
kind: "team-assistant",
session: {
// Declared at the flow level so the client block below sees it typed.
stateSchema: z.object({
messageCount: z.number().default(0),
}),
client: {
expose: ["messageCount"],
},
},
actions: {
chat: {
inputSchema: z.object({ message: z.string() }),
block: chatPipeline, // other state fields (mode, etc.) bubble up from blocks
},
},
});

messageCount is declared at the flow level because the flow itself reads it (in the client.expose list). Other fields — a mode flag a router uses, a lastModelUsed field a generator writes — stay declared on their blocks and merge in via bubbling. The rule of thumb: declare state at the flow level only when something on the flow config actually reads it.

Where to next​

  • State Operations — full operation reference: every helper, CAS semantics, version handling, ConcurrentModificationError.
  • Block State — request-scoped state any block can hold, addressed via ctx.self and ctx.parent.
  • State Targets and Parents — typed access to ancestor block state via targetStateSchemas and ctx.getTarget().
  • Sequencer State — state scoped to a sequencer's execution rather than identity, with a different durability boundary.
  • Resources — typed containers for structured data and rich content, with the same atomic operations as state.
  • Authentication — how userId and orgId reach a flow execution.