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.
Release check: Each run receives fresh mutable Flow variables. Top-level
constmeans non-exposed andletmeans exposed; neither is immutable.@runtimeand@secretcan 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 ornull.@readonlydisables editing surfaces but does not block Set Variable during execution. Per-user/device identity, universal preflight, and runtime@readonlyenforcement remain incomplete.
11.1 Local bindings and Flow variables
Section titled “11.1 Local bindings and Flow variables”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 = 0attempts = attempts + 1When 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 = 3Generated 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:
| Declaration | Current 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:
| Decorator | Meaning today |
|---|---|
@description("…") | Explain what the configurator must provide |
@category("…") | Group related values in configuration interfaces |
@readonly | Mark the variable non-editable in current authoring/configuration UI |
@runtime | Take the configured value from the runtime channel |
@secret | Hide 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")@readonlyconst 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")@runtimeconst incidentExportPath: PathThe 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 ─▶ executeA 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@runtimeconst openAiApiKey: stringThe 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.
Changing a hidden secret safely
Section titled “Changing a hidden secret safely”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.
11.6 Choose the right lifetime
Section titled “11.6 Choose the right lifetime”Choose by the cost of losing a value. Use this table before creating another Flow variable:
| Mechanism | Lifetime and ownership | Use it for | Do not use it for |
|---|---|---|---|
| Local binding | One body path in one run | Derived values and readable names | Cross-run state |
| Flow variable | Shared mutable state inside one run | Counters, flags, accumulators | Durable records |
| Exposed variable | Shared App/Board configuration default | URLs, limits, feature choices | Personal secrets |
| Runtime value | Local client profile/device configuration | Local paths, personal preferences, local BYOK | Shared unattended remote credentials |
| Event override | One configured Event boundary | Environment or trigger configuration | Arbitrary caller mutation |
| Cache | Cross-run but disposable, optionally expiring | Small hot values and recomputable results | Audit facts or irreplaceable data |
| App file | Durable object/blob storage | Documents and large reference datasets | Highly concurrent field updates |
| Database/Data Studio | Durable, queryable records | Sync state, entities, history, concurrent work | Temporary expression results |
| Chat/session state | One interaction context | Conversation continuity | App-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@runtimeconst openAiApiKey: stringOn 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.