Skip to content

Conversation

@androidfans
Copy link

@androidfans androidfans commented Nov 29, 2025

Support String Interpolation for Variables and Resources

Summary

This PR adds support for string interpolation of $var:, $res:, and $encrypted: references within string values. Previously, these references were only replaced when they appeared as the entire value. Now they can be embedded within strings, enabling use cases like "Bearer $var:api_token" or "https://api.example.com?key=$var:api_key".

Problem Statement

When using generated UI forms or HTTP request nodes, users often need to compose strings that include variable values. For example:

// Input in generated UI:
var1: "$var:f/Narration/jina_api_key"
api_key: "Bearer $var:f/Narration/jina_api_key"

Previously, the behavior was:

  • var1 → Replaced with actual value: "jina_4797cd5bb8f54314"
  • api_keyNOT replaced, remained as literal string: "Bearer $var:f/Narration/jina_api_key"

This limitation forced users to handle string concatenation in their scripts, which is problematic for generic HTTP request nodes where the code doesn't know which parameters need concatenation.

Solution

This PR implements string interpolation for variable/resource/encrypted references:

Before (Exact Match Only)

Value::String(y) if y.starts_with("$var:") => {
    // Only replaces if entire string is "$var:path"
}

After (String Interpolation)

Value::String(y) if y.starts_with("$var:") && !y.contains(' ') => {
    // Exact match: entire string is a variable reference
    // Returns the variable value (could be any JSON type)
}
Value::String(y) if (*RE_RES_VAR).is_match(&y) => {
    // String interpolation: scan and replace all occurrences
    // Always returns a string with substitutions made
}

Interpolation Pattern

The regex pattern \$var:([^\s"'\},\)\]&;]+) extracts variable paths, stopping at common delimiters:

  • Whitespace, quotes (", ')
  • JSON/array terminators (}, ,, ), ])
  • URL/query separators (&, ;)

This ensures proper extraction in contexts like:

  • JSON values: {"Authorization": "$var:token"}
  • URL parameters: ?key=$var:api_key&other=value
  • Arrays: ["$var:item1", "$var:item2"]
  • Function calls: func($var:arg)

Changes

Backend Worker (windmill-worker/src/common.rs)

  1. Added regex patterns for extracting variable/resource/encrypted references:
static ref RE_VAR_PATTERN: Regex = Regex::new(r#"\$var:([^\s"'\},\)\]&;]+)"#).unwrap();
static ref RE_RES_PATTERN: Regex = Regex::new(r#"\$res:([^\s"'\},\)\]&;]+)"#).unwrap();
static ref RE_ENCRYPTED_PATTERN: Regex = Regex::new(r#"\$encrypted:([^\s"'\},\)\]&;]+)"#).unwrap();
  1. Modified transform_json_value() to handle both exact matches and string interpolation:
    • Exact match (e.g., "$var:path") → Returns variable value as-is (preserves type)
    • String interpolation (e.g., "Bearer $var:token") → Scans string, replaces all matches, returns string

API Layer (windmill-api/src/resources.rs)

Applied the same changes to the API layer's transform_json_value() function to ensure consistency across all variable resolution paths.

Behavior

Exact Match (Unchanged)

// Input: "$var:f/config/timeout"
// Variable value: 30 (number)
// Output: 30 (preserves type)

String Interpolation (New)

// Input: "Bearer $var:f/auth/api_token"
// Variable value: "sk-abc123"
// Output: "Bearer sk-abc123" (string)

Multiple Substitutions (New)

// Input: "$var:greeting $var:name"
// Variables: greeting="Hello", name="World"
// Output: "Hello World"

Mixed Types (New)

// Input: "https://api.example.com?key=$var:api_key&user=$res:f/config/user_id"
// Output: "https://api.example.com?key=abc123&user=42"

Error Handling

  • If a referenced variable/resource doesn't exist, the job fails with a clear error message:
    Variable f/auth/api_token not found in string interpolation for `Authorization`: ...
    
  • Path validation is maintained (e.g., resources must have at least 2 path segments)

Testing

Tested with various scenarios:

  • ✅ Single variable in string
  • ✅ Multiple variables in same string
  • ✅ Mixed $var: and $res: references
  • ✅ JSON contexts with quotes, brackets, braces
  • ✅ URL query parameters with & separators
  • ✅ Array/function contexts
  • ✅ Whitespace and special character boundaries
  • ✅ Non-existent variable error handling

Backward Compatibility

Fully backward compatible

  • Exact match behavior unchanged: "$var:path" still returns the variable value with original type
  • Existing scripts and workflows continue to work without modification
  • Only adds new functionality for strings containing embedded references

Frontend Changes

No frontend changes required

All variable substitution happens on the backend. Frontend components that display or edit $var: references remain unchanged.

Related Issues

#7253
Fixes the issue where users couldn't use variable references within composed strings, particularly problematic for:

  • Generic HTTP request nodes
  • Authorization headers
  • API URL construction
  • Configuration templates

…d `$encrypted:` references within string values. Previously, these references were only replaced when they appeared as the entire value. Now they can be embedded within strings, enabling use cases like `"Bearer $var:api_token"` or `"https://api.example.com?key=$var:api_key"`.

When using generated UI forms or HTTP request nodes, users often need to compose strings that include variable values. For example:

```javascript
// Input in generated UI:
var1: "$var:f/Narration/jina_api_key"
api_key: "Bearer $var:f/Narration/jina_api_key"
```

Previously, the behavior was:
- ✅ `var1` → Replaced with actual value: `"jina_4797cd5bb8f54314"`
- ❌ `api_key` → **NOT replaced**, remained as literal string: `"Bearer $var:f/Narration/jina_api_key"`

This limitation forced users to handle string concatenation in their scripts, which is problematic for generic HTTP request nodes where the code doesn't know which parameters need concatenation.

This PR implements string interpolation for variable/resource/encrypted references:

```rust
Value::String(y) if y.starts_with("$var:") => {
    // Only replaces if entire string is "$var:path"
}
```

```rust
Value::String(y) if y.starts_with("$var:") && !y.contains(' ') => {
    // Exact match: entire string is a variable reference
    // Returns the variable value (could be any JSON type)
}
Value::String(y) if (*RE_RES_VAR).is_match(&y) => {
    // String interpolation: scan and replace all occurrences
    // Always returns a string with substitutions made
}
```

The regex pattern `\$var:([^\s"'\},\)\]&;]+)` extracts variable paths, stopping at common delimiters:
- Whitespace, quotes (`"`, `'`)
- JSON/array terminators (`}`, `,`, `)`, `]`)
- URL/query separators (`&`, `;`)

This ensures proper extraction in contexts like:
- JSON values: `{"Authorization": "$var:token"}`
- URL parameters: `?key=$var:api_key&other=value`
- Arrays: `["$var:item1", "$var:item2"]`
- Function calls: `func($var:arg)`

1. **Added regex patterns** for extracting variable/resource/encrypted references:
```rust
static ref RE_VAR_PATTERN: Regex = Regex::new(r#"\$var:([^\s"'\},\)\]&;]+)"#).unwrap();
static ref RE_RES_PATTERN: Regex = Regex::new(r#"\$res:([^\s"'\},\)\]&;]+)"#).unwrap();
static ref RE_ENCRYPTED_PATTERN: Regex = Regex::new(r#"\$encrypted:([^\s"'\},\)\]&;]+)"#).unwrap();
```

2. **Modified `transform_json_value()`** to handle both exact matches and string interpolation:
   - Exact match (e.g., `"$var:path"`) → Returns variable value as-is (preserves type)
   - String interpolation (e.g., `"Bearer $var:token"`) → Scans string, replaces all matches, returns string

Applied the same changes to the API layer's `transform_json_value()` function to ensure consistency across all variable resolution paths.

```javascript
// Input: "$var:f/config/timeout"
// Variable value: 30 (number)
// Output: 30 (preserves type)
```

```javascript
// Input: "Bearer $var:f/auth/api_token"
// Variable value: "sk-abc123"
// Output: "Bearer sk-abc123" (string)
```

```javascript
// Input: "$var:greeting $var:name"
// Variables: greeting="Hello", name="World"
// Output: "Hello World"
```

```javascript
// Input: "https://api.example.com?key=$var:api_key&user=$res:f/config/user_id"
// Output: "https://api.example.com?key=abc123&user=42"
```

- If a referenced variable/resource doesn't exist, the job fails with a clear error message:
  ```
  Variable f/auth/api_token not found in string interpolation for `Authorization`: ...
  ```
- Path validation is maintained (e.g., resources must have at least 2 path segments)

Tested with various scenarios:
- ✅ Single variable in string
- ✅ Multiple variables in same string
- ✅ Mixed `$var:` and `$res:` references
- ✅ JSON contexts with quotes, brackets, braces
- ✅ URL query parameters with `&` separators
- ✅ Array/function contexts
- ✅ Whitespace and special character boundaries
- ✅ Non-existent variable error handling

✅ **Fully backward compatible**

- Exact match behavior unchanged: `"$var:path"` still returns the variable value with original type
- Existing scripts and workflows continue to work without modification
- Only adds new functionality for strings containing embedded references

❌ **No frontend changes required**

All variable substitution happens on the backend. Frontend components that display or edit `$var:` references remain unchanged.

Fixes the issue where users couldn't use variable references within composed strings, particularly problematic for:
- Generic HTTP request nodes
- Authorization headers
- API URL construction
- Configuration templates

---

- `backend/windmill-worker/src/common.rs` - Worker-side variable transformation
- `backend/windmill-api/src/resources.rs` - API-side variable transformation
@github-actions
Copy link
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


rejectliu seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.


match client.get_variable_value(path).await {
Ok(value) => {
result = result.replace(full_match, &value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interpolation branch for string values (starting at line 287), the loops over RE_VAR_PATTERN and RE_RES_PATTERN use .replace() in each iteration. If a variable appears multiple times, this could trigger redundant lookups. Consider caching the replacement values to improve efficiency.

@androidfans androidfans changed the title fix: Support String Interpolation for Variables and Resources 7253 fix: Support String Interpolation for Variables and Resources Nov 29, 2025
@androidfans
Copy link
Author

This issue found when i am trying to migrate my script from N8N, to align function with n8n

@rubenfiszel
Copy link
Contributor

Thank you for the contribution.

I'm not convinced this is something we should allow. It shifts the complexity from being in the data structure/schema to being in the interpolation which is arguably worse.

In your example, api key should be one of the field and the logic of the script prepend it with Bearer where needed. If there are multiple cases to handle where sometimes it's Bearer, sometimes it's not, it should be handled with OneOf which we support already.

@androidfans
Copy link
Author

Thank you for the contribution.

I'm not convinced this is something we should allow. It shifts the complexity from being in the data structure/schema to being in the interpolation which is arguably worse.

In your example, api key should be one of the field and the logic of the script prepend it with Bearer where needed. If there are multiple cases to handle where sometimes it's Bearer, sometimes it's not, it should be handled with OneOf which we support already.

but if user need a common http request node support header set from ui like n8n does, that will be necessary

@rubenfiszel
Copy link
Contributor

@androidfans would you have the code of the script you have in mind?

@androidfans
Copy link
Author

androidfans commented Nov 29, 2025

@androidfans would you have the code of the script you have in mind?

interface Header {
  key: string;
  value: string;
}

export async function main(
  url: string,
  request_type: string,
  headers: Header[] = [],
  body: string = ""
) {
  const headersInstance = new Headers();
  headers.forEach(header => {
    if (header.key && header.value) {
      headersInstance.append(header.key, header.value);
    }
  });

  const method = request_type.toUpperCase();
  const hasBody = method !== 'GET' && method !== 'HEAD';
  const requestBody = (hasBody && body) ? body : undefined;

  const req = new Request(url, {
    method: method,
    headers: headersInstance,
    body: requestBody
  });

  try {
    const resp = await fetch(req);
    const responseText = await resp.text();
    let responseData: any = responseText;
    if (responseText) {
      try {
        responseData = JSON.parse(responseText);
      } catch (e) {
      }
    }

    return {
      response: responseData,
      status: resp.status,
      statusText: resp.statusText,
      ok: resp.ok
    };

  } catch (error) {
    throw new Error(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
  }
}

you can see the network node from n8n, support add header , and header string support insert value.

image

some main blocker blocks me migrate my work flow from n8n to windmill is

  1. can't get a powerfull http(s) node, need a lot of workaround code to achieve a simple function in n8n
  2. ai agent don't support multi-pass predefined message, like basic llm chain in n8n. if we wan't to achieve this , need write a ai agent node support add object into array(similiar to http header?). but prompt is too long, hard to write an multi-pass prompt array in js expression input method
  3. resource/variable mechanism looks powerful, but actual user experience not too good like n8n, due to lack of some function (for example : credential value can't concat to bearer string)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants