SimStudio AI Variable Resolution System and Block References Explained
SimStudio AI resolves workflow values in two distinct phases: block output references ({{blockId.outputName}}) are expanded first, then angle-bracket variables (<variableName>) are replaced, ensuring every block receives concrete data before execution begins.
The simstudioai/sim repository implements a deterministic resolution engine that bridges workflow definitions with runtime execution. This article examines how the variable resolution system and block reference syntax function at the source code level, based on the type definitions in packages/workflow-types and the realtime handlers in apps/realtime.
Variable Definition and Storage
Workflow variables are defined in the static workflow schema and persisted to the database before execution begins.
Schema Definition
In packages/workflow-types/src/workflow.ts, the Workflow interface declares an optional variables map that stores workflow-wide values accessible to any block:
export interface Workflow {
// ...
/** A map of workflow‑wide variables that can be referenced from any block. */
variables?: Record<string, Variable>;
}
Database Persistence
The variable map is stored as JSON in the database schema defined in packages/db/schema.ts:
variables: json('variables')
Runtime Loading
During execution, the realtime server loads these variables into memory. The handler in apps/realtime/src/handlers/variables.ts creates a plain JavaScript object attached to the execution context, making the values available to the resolution engine.
Variable Resolution Syntax and Implementation
Blocks embed variables using the angle-bracket notation:
<myVariable>
Resolution Algorithm
The resolution logic in apps/realtime/src/handlers/variables.ts scans input strings using the regex pattern <([^>]+)>. When a match is found, the engine looks up the captured name in the execution-time variables map:
function resolveVariables(input: string, vars: Record<string, unknown>) {
return input.replace(/<([^>]+)>/g, (_, name) => {
if (Object.prototype.hasOwnProperty.call(vars, name)) {
return String(vars[name]);
}
throw new Error(`Variable "${name}" not defined in workflow`);
});
}
If a variable is missing, the engine throws a descriptive error before the block's core logic executes. This guarantees that API calls and tool invocations receive literal values rather than unresolved placeholders.
Block Reference Resolution Mechanism
Blocks expose named outputs defined in their configuration, allowing downstream blocks to consume results through the double-brace syntax:
{{blockId.outputName}}
Output Declaration
Block output schemas are defined in packages/workflow-types/src/blocks.ts. Each block specifies which fields it makes available to subsequent steps in the workflow graph.
Runtime Resolution
The resolution routine resides in apps/realtime/src/handlers/subblocks.ts. The engine performs four distinct steps:
- Token Identification: Scan input strings for the pattern
{{blockId.outputName}} - Block Lookup: Retrieve the source block from the current execution graph
- Output Retrieval: Access the stored result map from completed blocks
- Value Substitution: Replace the placeholder with the actual value
The implementation follows this structure:
function resolveBlockRefs(
input: string,
executedBlocks: Record<string, BlockResult>
) {
return input.replace(/{{([^}.]+)\.([^}]+)}}/g, (_, blockId, outputKey) => {
const block = executedBlocks[blockId];
if (!block) throw new Error(`Block "${blockId}" not executed`);
if (!(outputKey in block.outputs))
throw new Error(`Output "${outputKey}" not found on block "${blockId}"`);
return String(block.outputs[outputKey]);
});
}
If a referenced block has not yet run, the engine throws a "dependency not satisfied" error, enforcing correct execution order.
Resolution Order and Nested References
The engine processes both resolution passes before a block's main logic executes. The specific order is:
- Block references (
{{...}}) are expanded first - Variables (
<...>) are resolved in the resulting string
This sequencing enables nested usage patterns:
{{someBlock.output}}-<myVariable>
The resolver first expands the block reference to produce a concrete string, then replaces any remaining angle-bracket tokens. This allows variables to customize or postfix block outputs.
Safety, Validation, and Auditing
Schema Validation
Both variable definitions and block output schemas are typed with Zod in the contract layer (apps/sim/lib/api/contracts/*). This ensures the runtime never receives values of unexpected shapes.
Error Handling
All resolution errors surface as ZodError-compatible messages. The API layer in apps/sim/lib/api/server formats these into 400 responses with descriptive explanations of which variable or block reference failed.
Audit Logging
Variable updates are logged via the audit system in packages/audit/src/log.ts using the WORKFLOW_VARIABLES_UPDATED action. This creates a permanent record of who modified workflow variables and when the changes occurred.
Code Examples
Referencing a Workflow Variable
{
"variables": {
"apiKey": { "type": "string", "value": "ABCD1234" }
},
"blocks": [
{
"id": "callStripe",
"type": "stripe/charge",
"params": {
"apiKey": "<apiKey>",
"amount": "5000"
}
}
]
}
During execution, the callStripe block receives the literal string "ABCD1234" for its apiKey parameter.
Chaining Block Outputs
{
"blocks": [
{
"id": "fetchUser",
"type": "http/get",
"params": { "url": "https://api.example.com/user/123" },
"outputs": { "responseBody": "string" }
},
{
"id": "parseUser",
"type": "json/parse",
"params": {
"json": "{{fetchUser.responseBody}}"
}
}
]
}
The parseUser block receives the raw HTTP response body that fetchUser stored under responseBody.
Combining Both Resolution Types
{
"variables": { "suffix": { "type": "string", "value": "_v2" } },
"blocks": [
{
"id": "makeName",
"type": "string/concat",
"params": { "parts": ["base", "<suffix>"] }
},
{
"id": "final",
"type": "log/print",
"params": { "message": "{{makeName.result}}" }
}
]
}
makeNameresolves<suffix>to"_v2"and concatenates to produce"base_v2"finalprints"base_v2"by referencing{{makeName.result}}
Summary
- Variable storage: Defined in
packages/workflow-types/src/workflow.ts, persisted inpackages/db/schema.ts, and loaded byapps/realtime/src/handlers/variables.ts - Variable syntax: Angle brackets (
<variableName>) resolved via regex in the variables handler - Block reference syntax: Double braces (
{{blockId.outputName}}) processed byapps/realtime/src/handlers/subblocks.ts - Execution order: Block references resolve first, then variables, enabling nested composition
- Safety: Zod schema validation, descriptive error messages, and audit logging via
packages/audit/src/log.ts
Frequently Asked Questions
What is the difference between angle brackets and double braces in SimStudio AI workflows?
Angle brackets (<variableName>) reference workflow variables stored in the execution context, while double braces ({{blockId.outputName}}) reference block outputs from previous steps in the workflow graph. Variables are defined statically in the workflow schema, whereas block references are dynamic values produced during execution.
How does the simstudioai/sim engine handle missing references?
The engine throws descriptive errors before block execution begins. If a variable is missing, the resolver in apps/realtime/src/handlers/variables.ts throws Variable "${name}" not defined in workflow. If a block reference points to an unexecuted block or missing output, apps/realtime/src/handlers/subblocks.ts throws either Block "${blockId}" not executed or Output "${outputKey}" not found. These errors propagate as 400 responses with precise location information.
Can I nest block references inside variables or vice versa?
Yes. The resolution system supports nested usage because it processes block references first, then variables. For example, {{fetchData.url}}-<environment> first expands the block reference to a URL string, then appends the environment variable suffix. However, you cannot reference a variable inside a block ID or output name—the {{...}} syntax requires literal block identifiers.
Where does simstudioai/sim store workflow variables during execution?
Variables are stored in three locations: (1) the database JSON field defined in packages/db/schema.ts for persistence, (2) the in-memory execution context created by apps/realtime/src/handlers/variables.ts during runtime, and (3) the audit logs in packages/audit/src/log.ts via the WORKFLOW_VARIABLES_UPDATED action for change tracking. The runtime copy is a plain JavaScript object consulted by the resolution regex before each block executes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →