Skip to content

State

State is what turns a document into an application.

A static document has content. An application has content that changes based on what happened before. In String, the difference between the two is state — variables that flow between actions and shape what the AI sees next.

No database. No backend session store. Just variables.


Every action produces a result. That result is available as Response until the next action runs.

/act.login --provider github
→ Response.body.access_token = "ghp_abc123..."
→ Response.body.agent = "agent01"

A response template stores parts of it as session variables:

```act.login.response
{token} = {Response.body.access_token}
{agent} = {Response.body.agent}
```

Now subsequent actions can use those values:

/act.get_repos --token {token}
/act.create_issue --token {token} --title "Bug report"

This is the loop that makes state work:

act → Response → response template stores {var} → act (using {var}) → ...

Each turn builds on the last. The AI doesn’t need to remember the access token in its context — it’s in the session. The AI just references {token} and String substitutes the value.


String has two kinds of variables with different lifecycles and purposes.

Ephemeral variables that live in a session. Created by response templates when actions run, consumed by subsequent actions.

{city} = {Response.body.city}
{temp} = {Response.body.temperature}
  • Set by: response templates (primary), AI /set (possible)
  • Scoped to: the topic session — each topic has its own set
  • Lifetime: the session — lost when the session closes
  • Storage: in-memory only
  • Used in: action URI path params, flag values, response output

Session variables are the working memory of a running app. A weather app calls /act.search, the response template stores {city} and {temp}, and subsequent actions reference them. When the session ends, they’re gone.

Durable variables stored on disk. Set by the human (via config file) or by the AI (via /set). Persist across sessions and restarts.

$API_KEY
$DEFAULT_REGION
$USER_EMAIL
  • Set by: human (config file) or AI (/set $VAR = "value")
  • Scoped to: app or app config
  • Lifetime: persistent until explicitly changed or deleted
  • Storage: file-backed (EnvStore)
  • Used in: action definitions (URI, headers, CLI commands)
  • Declared as requirements: requires: in app frontmatter

Persistent variables are the configuration layer. An API key set for one app is available to that app’s actions, but not to other apps. An app can declare what it requires, and String tells the AI what’s missing.


The two variable types serve different moments in the workflow.

{var} — runtime data flow:

The document developer designs the data pipeline. Response templates extract values from API responses and store them as {var}. Actions use those values. The AI doesn’t need to know the plumbing — it calls /act.search, and {city} is populated by the template.

Document developer writes: AI at runtime:
response template /act.search --name "Seoul"
{city} = {Response.body.city} → {city} is now "Seoul"
{temp} = {Response.body.temp} → {temp} is now "24"
/act.forecast --city {city}

$var — configuration and credentials:

Before the app runs, $var values need to be in place. The human sets API keys in the config. The AI sets app-specific parameters when required.

Human sets once: AI uses in actions:
$API_KEY = "sk-abc..." act.search → GET ...?key=$API_KEY
$DEFAULT_REGION = "KR" act.weather → ...&region=$DEFAULT_REGION

What goes where:

{var}$var
API response dataYesNo
API keys, credentialsNoYes
Agent preferencesNoYes
Intermediate computationYesNo
Cross-session configNoYes

Persistent variables are app-scoped. Each app gets its own env file; nothing is shared across apps, and the OS shell’s environment never leaks in.

~/.string/agents/{agent}/
apps/
weather/
env.json ← app:weather scope
seoul/
env.json ← app:weather:seoul scope (config)
moltbook/
env.json ← app:moltbook scope

App scopeapps/{app}/env.json holds variables for an app:

{
"MOLTBOOK_API_KEY": "moltbook_sk_..."
}

Config scopeapps/{app}/{config}/env.json holds variables for a specific configuration (app:weather:seoul). Overrides app scope for keys that appear in both.

Resolution — config → app, most specific wins:

config env → app env
(highest) (lowest)

When an action references $API_KEY, String checks the config scope, then the app scope. No global fallback. No process.env fallback. A reference that doesn’t resolve in the current app’s scope returns INVALID_PAYLOAD with a “set it from this app session” hint.

Why no global. A shared env would let any installed app read keys set by another. The leak is closed by design: an app’s $X is unreachable from any other app, and the daemon’s shell environment is opaque to action substitution. The cost — a key needed by N apps must be set N times — is the price of isolation.

System vars — daemon-defined context vars are still available in every action: $HOME (the String per-agent.home, not the OS HOME), $CWD, $CURRENT_FILE, $CURRENT_URI, $CURRENT_TARGET, $CURRENT_BLOCK, $ARGS. These are supplied fresh per call and live above the app’s own env in the resolution chain.

Setting a var/set $X = "..." only works from an app session (app:NAME or app:NAME:CONFIG). In main, bash:*, or hub topics it returns INVALID_TARGET — there’s no app to scope to.


Apps declare which $var values they need with requires: in frontmatter:

---
requires:
- API_KEY
- DEFAULT_REGION
---

When the app is opened, String checks whether the required variables are set. If $API_KEY is missing, the rendered page starts with a setup hint:

[!] Missing required env: $API_KEY
Set: /set $API_KEY = "..."

The AI then knows to ask the agent for the value and set it:

/set $API_KEY = "sk-abc123"

Only $var can be declared as requirements. {var} is runtime data — it doesn’t make sense to require it before the app runs.


AI or human sets persistent variables with /set:

<𝒞=string:app:weather:korea>
/set $API_KEY = "sk-abc123"
</𝒞>

The scope is determined by the current topic:

TopicStores in
mainNot allowed for $VAR; use an app topic
app:weatherApp scope (apps/weather/env.json)
app:weather:koreaConfig scope (apps/weather/korea/env.json)

Multiple variables at once:

<𝒞=string:app:weather>
/set
$API_KEY = "sk-abc123"
$DEFAULT_REGION = "KR"
</𝒞>

AI can also set session variables directly, though the recommended path is through response templates:

/set {city} = "Seoul"

For multiline values, use a fenced code block with the variable name:

<𝒞=string:app:search>
/set
```{prompt}
You are a research assistant.
Summarize the results in bullet points.
Keep it under 200 words.

</𝒞>

Single and double quotes both work for inline values:

/set {region} = ‘TH’ /set $REGION = “TH”

### Listing variables
`/set` with no arguments shows all variables in the current scope:

/set → {city} = “Seoul” → {temp} = “24” → $API_KEY = “sk-abc123” → $DEFAULT_REGION = “KR”

---
## Substitution
Both variable types are substituted by the String runtime at
execution time.
**In action definitions (document-level):**
````markdown
```act.search
GET https://api.weather.com/search?key=$API_KEY
name: string (required)
Here `$API_KEY` is resolved from the EnvStore when the action runs.
**In AI commands (flag values):**
```
/act.search --name {city}
```
Here `{city}` is resolved from the session. The AI references the
variable; String substitutes.
**In response templates:**
````markdown
```act.search.response
{city} = {Response.body.name}
Current weather in {city}: {Response.body.description}
```

Only {var} can be assigned in response templates. $var cannot be assigned here — it’s configuration, not runtime data.

Substitution summary:

Where{var}$var
Action definition (URI, headers)SubstitutedSubstituted
AI command flag valuesSubstitutedSubstituted
Response template assignmentYes ({var} = {Response...})No
Response template outputSubstitutedNot applicable

Traditional applications separate code from state and manage state through databases, session stores, and caches. String collapses this into two layers.

An SFMD document defines actions (code). Variables hold state — {var} for runtime data, $var for configuration. Together, they form an application:

Document (actions + templates)
+
Variables ({var} session + $var persistent)
=
Application

Consider an email client:

/open app:gmail:work
Session state (from actions):
{token} = "ya29.abc..." ← from act.login.response
{inbox_count} = "12" ← from act.refresh.response
{current_folder} = "inbox" ← from act.switch.response
Persistent config (from /set or config file):
$GMAIL_CLIENT_ID = "..." ← set once, used by act.login
$GMAIL_CLIENT_SECRET = "..." ← set once, used by act.login

The document defines what actions exist (login, refresh, compose, search). Session {var} tracks where the agent is (inbox, 12 messages, authenticated). Persistent $var holds credentials set once and reused across sessions.

The AI navigates it the same way it navigates any String document — /open to see, /act to do. The difference is that state accumulates across turns, making each action aware of what came before.


ConceptRule
{variable}Session-scoped, set by response templates, ephemeral
$variablePersistent, set by human or AI, file-backed
ResponseLast action result, overwritten on next action
Response templateCan assign {var} only — not $var
requires:Declares required app $var — not {var}
$var cascadeconfig scope > app scope
/setSets {var} (session) or $var (persistent)
Core insightDocument + variables = application