Skip to content

11. State, Configuration, Runtime Values, and Secrets

An API address and retry limit are configuration. An OpenAI key is a credential. The last successful synchronization may be durable state, while a ten-gigabyte dataset needs storage built for its size. Calling all of them “values” hides the important differences.

Before choosing syntax, ask who owns this value, and how long must it remain true? Keep state in the narrowest scope that covers its lifetime. Durable facts need durable storage, and credentials stay out of source.

A value-placement map separates values used inside one run, configuration inputs, cross-run state, and credentials, then assigns each to the narrowest suitable mechanism.
Owner and lifetime choose the mechanism. Credentials add a trust boundary: secret metadata may appear in FlowScript, while the stored value remains in protected configuration.

Release check: Each run receives fresh mutable Flow variables. Top-level const means non-exposed and let means exposed; neither is immutable. @runtime and @secret can be combined. Web and Desktop store values in local IndexedDB by App and variable, so they are device-profile local, not explicitly user-keyed. Interactive paths prompt for missing values, while direct and unattended paths may still use a Board/Event default or null. @readonly disables editing surfaces but does not block Set Variable during execution. Per-user/device identity, universal preflight, and runtime @readonly enforcement remain incomplete.

Inside a function or Event body, a binding usually names a value produced by the graph:

const normalized = report.trim()
const urgent = normalized.contains({
substring: "production is on hold",
ignoreCase: true,
})

normalized names the String Trim output; urgent names the Boolean from String Contains. They are graph bindings, so consumers connect directly to those producer pins.

A mutable local can model an accumulator:

let attempts = 0
attempts = attempts + 1

When paths need shared mutable state, the reconciler can materialize a local variable and Get/Set nodes. It lasts for one run; the next Event starts from configured defaults.

Top-level declarations describe Board variables:

const urgentPhrase = "production is on hold"
let retryLimit = 3

Generated Get and Set operations access these variables throughout the Flow. Each run has its own map, so concurrent invocations do not communicate through retryLimit, and writes do not change the next run’s declaration.

This isolation is useful for parallel invocations: one incident can change its in-memory retry limit without altering another incident already in flight.

This makes Flow variables suitable for:

  • values shared by several nodes during one invocation;
  • counters and flags used by control flow;
  • arrays or Structs accumulated during a run; and
  • configured defaults that the Flow may temporarily transform.

Anything that must survive an invocation, such as sync history, balances, inventory, or job state, belongs in App Storage or a database.

11.2 Top-level const and let describe exposure

Section titled “11.2 Top-level const and let describe exposure”

TypeScript readers need to unlearn one association at document scope:

DeclarationCurrent FlowScript meaning
const value = …Create a non-exposed Board variable
let value = …Create an exposed Board variable

Both runtime values are mutable today. The difference is configuration visibility.

An editable, exposed let appears in App Configuration. Authorized people can change it without opening FlowScript or rearranging the Flow. Events may carry permitted overrides. App permissions decide who can make either change.

For the Incident App, these are reasonable exposed defaults:

@description("Base URL used by the incident AI provider")
@category("Incident AI")
let apiBaseUrl = "https://api.openai.com/v1"
@description("Maximum number of provider attempts")
@category("Incident AI")
let retryLimit = 3
@description("Language used for generated incident explanations")
@category("Incident AI")
let preferredLanguage = "en"

The Flow author chooses what is configurable; App configurators set shared defaults. Knowing a non-exposed variable’s ID must not grant a caller permission to rewrite it.

Changing an exposed default changes Board configuration for everyone. Use runtime configuration for a personal or device-specific language. Configure production and development at their Event or deployment boundaries instead of detecting the environment through hidden assumptions.

11.3 Decorators carry platform consequences

Section titled “11.3 Decorators carry platform consequences”

Variable decorators are metadata that both authoring views can preserve:

DecoratorMeaning today
@description("…")Explain what the configurator must provide
@category("…")Group related values in configuration interfaces
@readonlyMark the variable non-editable in current authoring/configuration UI
@runtimeTake the configured value from the runtime channel
@secretHide the value from FlowScript and sensitive authoring/read paths
@schema("…")Attach legacy inline Struct schema metadata

Use a named interface when source should describe a Struct contract; @schema is legacy metadata. Decorators sit immediately above the declaration. Canonical order is description, category, legacy schema, secret, readonly, runtime.

@readonly is currently weaker than its name

Section titled “@readonly is currently weaker than its name”

This declaration describes a value that App configuration should not edit:

@description("Provider selected by the Flow author")
@readonly
const providerName = "OpenAI"

Today, @readonly sets the Board variable’s editable flag to false. UI edits are disabled, yet a Set Variable node can still change the value during a run. Until runtime enforcement exists, @readonly provides neither a security boundary nor an integrity guarantee.

Top-level const controls exposure; @readonly expresses mutation policy. A hidden value can be mutable, and an exposed reference value may need to be immutable.

11.4 Runtime configuration is runner-specific input

Section titled “11.4 Runtime configuration is runner-specific input”

Use @runtime when the Flow definition should contain the contract but not one shared value:

@description("Path to the local incident export directory")
@runtime
const incidentExportPath: Path

The Flow retains the contract while Web and Desktop store the configured JSON value outside it in local IndexedDB. A saved value overrides the Board default for that run. Remote payloads may include non-secret runtime values.

The intended scope is per user and device. Today’s key contains App ID and variable ID inside the browser or Desktop profile. It isolates profiles, but cannot distinguish two Flow-Like accounts in one profile. A strict user-and-device promise requires a different key.

Treat the current store as local application state tied to that profile, not as an account-level configuration service.

Interactive execution currently follows a useful preflight:

Run requested
│
├─ saved records present ─▶ execute
│
└─ values missing ─▶ configuration dialog ─▶ save ─▶ execute

A required runtime value should be valid before any node runs, whether execution is interactive, scheduled, API-driven, internal, or agent-initiated. The interactive service currently checks only that a record exists. Core runtime does not enforce universal presence; without an override, it may resolve Event configuration, the Board default, or null. Universal preflight remains future work.

11.5 Secrets never become an authoring channel

Section titled “11.5 Secrets never become an authoring channel”

A BYOK credential can combine @secret and @runtime:

@description("Bring-your-own OpenAI API key for local execution")
@category("Incident AI")
@secret
@runtime
const openAiApiKey: string

The declaration has no value. FlowScript rejects a non-empty secret initializer, and rendering omits stored secret values. Opening, copying, sending, or editing source therefore reveals none.

For a local interactive run, Desktop can prompt once and save the value locally. The masked field can be revealed deliberately. Device-local storage alone does not promise encryption at rest, so the operating-system account and application data still need protection.

Standard clients remove local secrets from remote payloads. Remote BYOK therefore needs trusted server-side Event configuration. Event reads return blank secret values, and unrelated edits preserve stored values. Deployment determines at-rest protection; masking alone says nothing about it.

Filtering also prevents a browser or Desktop client from casually forwarding a device credential to a hosted executor.

The OpenAI provider call can then consume the value like any other typed input:

const model = ai::provider::openai({
provider: "OpenAI",
endpoint: apiBaseUrl,
apiKey: openAiApiKey,
})

The Flow declares the credential contract, a trusted configuration surface receives the value, and the provider node consumes it without source embedding. Local storage does not make that key available to every remote execution mode.

For hosted OpenAI BYOK, use a private provider/model profile. The server encrypts its credential, keeps it apart from public model metadata and ordinary profile responses, and hydrates it only in the trusted execution path. This also avoids copying a key into every Flow. Offline Desktop must download the credential to local settings, so device-profile protection still matters.

Secret metadata reduces exposure, but no universal redaction pass can recognize credentials copied into arbitrary messages. Never log, ticket, interpolate, or return a key.

The editor cannot validate a hidden value. FlowScript will not change a secret’s type, container shape, or schema while preserving an unknown default. To declassify or replace it, first use the guarded clear/declassify path, then make the ordinary type or value change explicitly.

Source and AI tools may change the credential contract without gaining authority to read or write the credential.

Choose by the cost of losing a value. Use this table before creating another Flow variable:

MechanismLifetime and ownershipUse it forDo not use it for
Local bindingOne body path in one runDerived values and readable namesCross-run state
Flow variableShared mutable state inside one runCounters, flags, accumulatorsDurable records
Exposed variableShared App/Board configuration defaultURLs, limits, feature choicesPersonal secrets
Runtime valueLocal client profile/device configurationLocal paths, personal preferences, local BYOKShared unattended remote credentials
Event overrideOne configured Event boundaryEnvironment or trigger configurationArbitrary caller mutation
CacheCross-run but disposable, optionally expiringSmall hot values and recomputable resultsAudit facts or irreplaceable data
App fileDurable object/blob storageDocuments and large reference datasetsHighly concurrent field updates
Database/Data StudioDurable, queryable recordsSync state, entities, history, concurrent workTemporary expression results
Chat/session stateOne interaction contextConversation continuityApp-wide truth

The key-value cache supports App and user scopes, namespaces, expiration, deletion, and values of roughly one mebibyte or less. It is separate from the evictable file cache directory. Cache lastSuccessfulSync only when loss causes a safe repeat sync. If loss could skip work, repeat an irreversible effect, impair recovery, or break an audit obligation, use a durable database record with the required retention policy.

For reference data, use an App file when the dataset is large, versioned, and mostly read-only. Cache small derivatives that can be recreated. Use a database for records that are updated, filtered, joined, governed, or changed concurrently.

One application may combine a source file, normalized database records, and a hot lookup cache. They still share the same App, permissions, runtime, and evidence model.

Flow-Like’s native data path can retain storage versions, but optimization may prune them. Set cleanup and retention policy deliberately. Regulation or recovery that requires an immutable journal needs an explicit audit model.

A local chat session belongs to one conversation. Current “global” chat state spans chats for the same App/Event on one local client. Use it for conversation continuity, never organization-wide or cross-device truth.

11.7 Read the complete configuration example

Section titled “11.7 Read the complete configuration example”

The canonical Chapter 11 fixture keeps configuration and a BYOK key visible without putting any credential value in the repository:

@description("Base URL used by the incident AI provider")
@category("Incident AI")
let apiBaseUrl = "https://api.openai.com/v1"
@description("Maximum number of provider attempts")
@category("Incident AI")
let retryLimit = 3
@description("Bring-your-own OpenAI API key for local execution")
@category("Incident AI")
@secret
@runtime
const openAiApiKey: string

On the Board, these become typed variables with generated Get/Set operations. The exposed values appear in App configuration. The runtime secret appears in the device-local configuration surface and is absent from rendered source. The provider builder consumes those values through ordinary typed pins.

The fixture omits lastSuccessfulSync and reference data from global variables. Their longer lifetimes require appropriate storage.