diff --git a/backend/.sqlx/query-2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943.json b/backend/.sqlx/query-2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943.json new file mode 100644 index 0000000000..8c5f43ab07 --- /dev/null +++ b/backend/.sqlx/query-2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n CASE\n WHEN flow_version.id IS NOT NULL THEN\n flow_version.value -> 'flow_env' -> $3\n ELSE\n root_job.raw_flow -> 'flow_env' -> $3\n END AS \"flow_env: sqlx::types::Json>\"\n FROM\n v2_job current_job\n JOIN\n v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)\n AND root_job.workspace_id = current_job.workspace_id\n LEFT JOIN\n flow_version ON flow_version.id = root_job.runnable_id\n AND flow_version.path = root_job.runnable_path\n AND flow_version.workspace_id = root_job.workspace_id\n WHERE\n current_job.id = $1 AND\n current_job.workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "flow_env: sqlx::types::Json>", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943" +} diff --git a/backend/.sqlx/query-c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154.json b/backend/.sqlx/query-c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154.json deleted file mode 100644 index be352ce88e..0000000000 --- a/backend/.sqlx/query-c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n CASE\n WHEN flow_version.id IS NOT NULL THEN\n (flow_version.value -> 'flow_env' -> $3) #> $4\n ELSE\n (root_job.raw_flow -> 'flow_env' -> $3) #> $4\n END AS \"flow_env: sqlx::types::Json>\"\n FROM\n v2_job current_job\n JOIN\n v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)\n AND root_job.workspace_id = current_job.workspace_id\n LEFT JOIN\n flow_version ON flow_version.id = root_job.runnable_id\n AND flow_version.path = root_job.runnable_path\n AND flow_version.workspace_id = root_job.workspace_id\n WHERE\n current_job.id = $1 AND\n current_job.workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "flow_env: sqlx::types::Json>", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "TextArray" - ] - }, - "nullable": [ - null - ] - }, - "hash": "c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154" -} diff --git a/backend/windmill-api/openapi-deref.yaml b/backend/windmill-api/openapi-deref.yaml index 1a036f6dc9..7f04a7287e 100644 --- a/backend/windmill-api/openapi-deref.yaml +++ b/backend/windmill-api/openapi-deref.yaml @@ -8857,9 +8857,8 @@ paths: type: boolean flow_env: type: object - description: Environment variables available to all steps - additionalProperties: - type: string + description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)." + additionalProperties: {} priority: type: number description: Execution priority (higher numbers run first) @@ -14644,9 +14643,8 @@ paths: type: boolean flow_env: type: object - description: Environment variables available to all steps - additionalProperties: - type: string + description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)." + additionalProperties: {} priority: type: number description: Execution priority (higher numbers run first) diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index d58dcd3c69..ac5a9a306b 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -448,14 +448,15 @@ async fn get_flow_env_by_flow_job_id( Path((w_id, flow_job_id, var_name)): Path<(String, Uuid, String)>, Query(JsonPath { json_path, .. }): Query, ) -> windmill_common::error::JsonResult> { - let flow_env = sqlx::query_scalar!( + // Fetch raw value (without json_path) to check for $var:/$res: references + let raw_value = sqlx::query_scalar!( r#" SELECT CASE WHEN flow_version.id IS NOT NULL THEN - (flow_version.value -> 'flow_env' -> $3) #> $4 + flow_version.value -> 'flow_env' -> $3 ELSE - (root_job.raw_flow -> 'flow_env' -> $3) #> $4 + root_job.raw_flow -> 'flow_env' -> $3 END AS "flow_env: sqlx::types::Json>" FROM v2_job current_job @@ -472,16 +473,86 @@ async fn get_flow_env_by_flow_job_id( flow_job_id, w_id, var_name, - json_path - .as_ref() - .map(|x| x.split(".").collect::>()) - .unwrap_or_default() as Vec<&str>, ) .fetch_optional(&db) .await? - .map(|r| r.map(|x| x.0)) - .flatten() - .unwrap_or_else(|| to_raw_value(&serde_json::Value::Null)); + .and_then(|r| r.map(|x| x.0)); + + // Resolve $var:/$res: references if present + let resolved = if let Some(raw) = raw_value { + let raw_str = raw.get(); + let db_authed = windmill_common::db::DbWithOptAuthed::::from_authed( + &authed, + db.clone(), + None, + ); + if let Some(path) = raw_str + .strip_prefix("\"$var:") + .and_then(|s| s.strip_suffix("\"")) + { + match windmill_store::variables::get_value_internal(&db_authed, &w_id, path, false) + .await + { + Ok(val) => to_raw_value(&serde_json::Value::String(val)), + Err(e) => { + tracing::warn!("Failed to resolve flow_env variable $var:{path}: {e}"); + raw + } + } + } else if let Some(path) = raw_str + .strip_prefix("\"$res:") + .and_then(|s| s.strip_suffix("\"")) + { + match windmill_store::resources::get_resource_value_interpolated_internal( + &db_authed, + &w_id, + path, + Some(flow_job_id), + Some(&tokened.token), + false, + ) + .await + { + Ok(Some(val)) => to_raw_value(&val), + Ok(None) => { + tracing::warn!( + "Failed to resolve flow_env resource $res:{path}: resource not found" + ); + raw + } + Err(e) => { + tracing::warn!("Failed to resolve flow_env resource $res:{path}: {e}"); + raw + } + } + } else { + raw + } + } else { + to_raw_value(&serde_json::Value::Null) + }; + + // Apply json_path navigation on the (possibly resolved) value + let flow_env = if let Some(ref jp) = json_path { + let mut value: serde_json::Value = + serde_json::from_str(resolved.get()).unwrap_or(serde_json::Value::Null); + for part in jp.split('.') { + value = match value { + serde_json::Value::Object(ref mut map) => { + map.remove(part).unwrap_or(serde_json::Value::Null) + } + serde_json::Value::Array(ref arr) => part + .parse::() + .ok() + .and_then(|i| arr.get(i).cloned()) + .unwrap_or(serde_json::Value::Null), + _ => serde_json::Value::Null, + }; + } + to_raw_value(&value) + } else { + resolved + }; log_job_view( &db, diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index d0b7fc7a58..33dafbb482 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; -use crate::common::{cached_result_path, get_root_job_id, save_in_cache}; +use crate::common::{cached_result_path, get_root_job_id, save_in_cache, transform_json}; use crate::js_eval::{eval_timeout, IdContext}; use crate::worker_utils::get_tag_and_concurrency; use crate::{ @@ -53,7 +53,7 @@ use windmill_common::runnable_settings::{ use windmill_common::scripts::{ScriptHash, ScriptRunnableSettingsInline}; use windmill_common::users::username_to_permissioned_as; use windmill_common::utils::WarnAfterExt; -use windmill_common::worker::to_raw_value; +use windmill_common::worker::{to_raw_value, Connection}; use windmill_common::{ add_time, get_latest_flow_version_info_for_path, get_script_info_for_hash, FlowVersionInfo, ScriptHashInfo, DB, @@ -2245,6 +2245,35 @@ pub async fn handle_flow( killpill_rx: &tokio::sync::broadcast::Receiver<()>, ) -> anyhow::Result<()> { let flow = flow_data.value(); + + // Resolve $var: and $res: references in flow_env. + // We resolve into a separate variable to avoid cloning the entire FlowValue + // (which includes modules, failure_module, etc.) just to replace flow_env. + let resolved_env; + let flow_env = if let Some(ref env) = flow.flow_env { + match transform_json( + client, + &flow_job.workspace_id, + env, + &flow_job, + &Connection::Sql(db.clone()), + ) + .await + { + Ok(Some(resolved)) => { + resolved_env = resolved; + Some(&resolved_env) + } + Ok(None) => flow.flow_env.as_ref(), + Err(e) => { + tracing::warn!("Failed to resolve flow_env references: {e}"); + flow.flow_env.as_ref() + } + } + } else { + None + }; + let status = flow_job .parse_flow_status() .with_context(|| "Unable to parse flow status")?; @@ -2348,6 +2377,7 @@ pub async fn handle_flow( flow_job, status, flow, + flow_env, db, client, last_result.clone(), @@ -2448,6 +2478,7 @@ async fn push_next_flow_job( flow_job: Arc, mut status: FlowStatus, flow: &FlowValue, + flow_env: Option<&HashMap>>, db: &sqlx::Pool, client: &AuthedClient, last_job_result: Option>>, @@ -2580,7 +2611,7 @@ async fn push_next_flow_job( let skip = compute_bool_from_expr( &skip_expr, arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, Arc::new(to_raw_value(&json!("{}"))), None, None, @@ -2705,7 +2736,7 @@ async fn push_next_flow_job( expr.to_string(), context, Some(arc_flow_job_args.clone()), - flow.flow_env.as_ref(), + flow_env, None, None, None @@ -2966,7 +2997,7 @@ async fn push_next_flow_job( &input_transform, arc_last_job_result.clone(), Some(arc_flow_job_args.clone()), - flow.flow_env.as_ref(), + flow_env, Some(client), None, ) @@ -3004,7 +3035,7 @@ async fn push_next_flow_job( &status.retry, arc_last_job_result.clone(), arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, Some(client), ) .await? @@ -3092,7 +3123,7 @@ async fn push_next_flow_job( compute_bool_from_expr( &skip_if.expr, arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), None, Some(&idcontext), @@ -3182,7 +3213,7 @@ async fn push_next_flow_job( }; transform_input( arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), input_transforms, resumes.clone(), @@ -3209,7 +3240,7 @@ async fn push_next_flow_job( let next_flow_transform = compute_next_flow_transform( arc_flow_job_args.clone(), arc_last_job_result.clone(), - flow.flow_env.as_ref(), + flow_env, &flow_job, &flow, transform_context, @@ -3373,7 +3404,7 @@ async fn push_next_flow_job( let ctx = get_transform_context(&flow_job, "", &status); let ti = transform_input( Marc::new(args), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), input_transforms, resumes.clone(), @@ -3428,7 +3459,7 @@ async fn push_next_flow_job( let ctx = get_transform_context(&flow_job, &previous_id, &status); let ti = transform_input( Marc::new(hm), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), input_transforms, resumes.clone(), @@ -3546,7 +3577,7 @@ async fn push_next_flow_job( timeout_transform, arc_last_job_result.clone(), Some(arc_flow_job_args.clone()), - flow.flow_env.as_ref(), + flow_env, Some(client), Some(&ctx), ) @@ -3625,7 +3656,7 @@ async fn push_next_flow_job( parallelism_transform, arc_last_job_result.clone(), Some(arc_flow_job_args.clone()), - flow.flow_env.as_ref(), + flow_env, Some(client), Some(&ctx), ) @@ -4461,7 +4492,7 @@ async fn compute_next_flow_transform( let pred = compute_bool_from_expr( &b.expr, arc_flow_job_args.clone(), - flow.flow_env.as_ref(), + flow_env, arc_last_job_result.clone(), None, Some(&idcontext), diff --git a/frontend/src/lib/components/flows/content/FlowEnvironmentVariables.svelte b/frontend/src/lib/components/flows/content/FlowEnvironmentVariables.svelte index ce51b8dc77..3b6b867abe 100644 --- a/frontend/src/lib/components/flows/content/FlowEnvironmentVariables.svelte +++ b/frontend/src/lib/components/flows/content/FlowEnvironmentVariables.svelte @@ -5,17 +5,21 @@ import { writable } from 'svelte/store' import type { FlowEditorContext } from '../types' import { Button } from '$lib/components/common' - import { Plus, Trash2 } from 'lucide-svelte' + import { DollarSign, Plus, Trash2 } from 'lucide-svelte' import FlowCard from '../common/FlowCard.svelte' import JsonEditor from '$lib/components/JsonEditor.svelte' import Label from '$lib/components/Label.svelte' import Select from '$lib/components/select/Select.svelte' + import ItemPicker from '$lib/components/ItemPicker.svelte' + import ResourcePicker from '$lib/components/ResourcePicker.svelte' + import { VariableService } from '$lib/gen' + import { workspaceStore } from '$lib/stores' interface Props { noEditor: boolean } - type EnvVarType = 'string' | 'json' + type EnvVarType = 'string' | 'json' | 'resource' interface EnvVarEntry { id: string @@ -37,6 +41,9 @@ function determineValueType(value: any): EnvVarType { if (typeof value === 'string') { + if (value.startsWith('$res:')) { + return 'resource' + } try { JSON.parse(value) return value.trim().startsWith('{') || @@ -53,30 +60,51 @@ let flowEnvTypes = $state>({}) - const typeOptions = [ - { label: 'String', value: 'string' as EnvVarType }, - { label: 'JSON', value: 'json' as EnvVarType } + const typeOptions: { label: string; value: EnvVarType }[] = [ + { label: 'String', value: 'string' }, + { label: 'JSON', value: 'json' }, + { label: 'Resource', value: 'resource' } ] + // Track resource paths separately for bind:value with ResourcePicker + let resourcePaths = $state>({}) + + // Initialize resourcePaths from existing flow_env values + for (const [key, value] of Object.entries(flowStore.val.value.flow_env || {})) { + if (typeof value === 'string' && value.startsWith('$res:')) { + resourcePaths[key] = value.substring('$res:'.length) + } + } + + // Initialize types for new keys and sync resourcePaths → flow_env $effect(() => { for (const [key, value] of flowEnvVarsMap.entries()) { if (!flowEnvTypes[key]) { flowEnvTypes[key] = determineValueType(value) } } - }) - - $effect(() => { - for (const [key, type] of Object.entries(flowEnvTypes)) { - if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) { - const currentType = determineValueType(flowStore.val.value.flow_env[key]) - if (currentType !== type) { - updateEnvType(key, type) + for (const [key, path] of Object.entries(resourcePaths)) { + if (flowStore.val.value.flow_env && flowEnvTypes[key] === 'resource') { + const newVal = '$res:' + (path || '') + if (flowStore.val.value.flow_env[key] !== newVal) { + flowStore.val.value.flow_env[key] = newVal + flowStore.val = flowStore.val } } } }) + // Convert values when user changes the type dropdown + let prevTypes: Record = {} + $effect(() => { + for (const [key, type] of Object.entries(flowEnvTypes)) { + if (prevTypes[key] && prevTypes[key] !== type) { + updateEnvType(key, type) + } + prevTypes[key] = type + } + }) + let flowEnvEntries = $derived( Array.from(flowEnvVarsMap.entries()).map(([key, value]): EnvVarEntry => { const stringValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2) @@ -113,6 +141,7 @@ if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) { delete flowStore.val.value.flow_env[key] delete flowEnvTypes[key] + delete resourcePaths[key] flowStore.val = flowStore.val } } @@ -150,32 +179,55 @@ flowStore.val.value.flow_env = newEnvVars delete flowEnvTypes[oldKey] flowEnvTypes[newKey] = type + + // Move resource path if applicable + if (type === 'resource' && oldKey in resourcePaths) { + resourcePaths[newKey] = resourcePaths[oldKey] + delete resourcePaths[oldKey] + } + flowStore.val = flowStore.val } } function updateEnvType(key: string, newType: EnvVarType) { if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) { - const currentValue = flowStore.val.value.flow_env[key] - const stringValue = - typeof currentValue === 'string' ? currentValue : JSON.stringify(currentValue, null, 2) - flowEnvTypes[key] = newType - if (newType === 'json') { - try { - const parsed = JSON.parse(stringValue) - flowStore.val.value.flow_env[key] = parsed - } catch { - flowStore.val.value.flow_env[key] = stringValue + if (newType === 'resource') { + flowStore.val.value.flow_env[key] = '$res:' + resourcePaths[key] = '' + } else if (newType === 'json') { + delete resourcePaths[key] + const currentValue = flowStore.val.value.flow_env[key] + if (typeof currentValue === 'string') { + try { + flowStore.val.value.flow_env[key] = JSON.parse(currentValue) + } catch { + // keep as string if not valid JSON + } } } else { - flowStore.val.value.flow_env[key] = stringValue + delete resourcePaths[key] + const currentValue = flowStore.val.value.flow_env[key] + if (typeof currentValue !== 'string') { + flowStore.val.value.flow_env[key] = JSON.stringify(currentValue, null, 2) + } } flowStore.val = flowStore.val } } + function setVarPath(key: string, path: string) { + if (flowStore.val.value.flow_env) { + flowStore.val.value.flow_env[key] = '$var:' + path + flowStore.val = flowStore.val + } + } + + let variablePicker: ItemPicker | undefined = $state(undefined) + let pickForKey: string | undefined = $state(undefined) + setContext('PropPickerWrapper', { inputMatches: writable(undefined), connectProp: () => {}, @@ -192,8 +244,8 @@ Flow envs can be referenced in any flow step input using the syntax{' '} flow_env.VARIABLE_NAME or flow_env["VARIABLE_NAME"]. These variables are available in the property picker and can be used in JavaScript expressions and - input bindings. You can choose between String or JSON types for each variable - JSON types - allow complex data structures. + input bindings. String values can link to workspace variables using the button. Resource type references workspace resources resolved at runtime. {#if flowEnvEntries.length === 0} @@ -246,10 +298,13 @@ {/if} -
- - - {#if entry.type === 'json'} +
+ {/each} @@ -285,3 +367,20 @@ + + { + if (pickForKey) { + setVarPath(pickForKey, path) + pickForKey = undefined + } + }} + itemName="Variable" + extraField="path" + loadItems={async () => + (await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({ + name: x.path, + ...x + }))} +/> diff --git a/frontend/src/lib/components/flows/propPicker/PropPickerWrapper.svelte b/frontend/src/lib/components/flows/propPicker/PropPickerWrapper.svelte index 6910fabbb5..ead5ad9ad3 100644 --- a/frontend/src/lib/components/flows/propPicker/PropPickerWrapper.svelte +++ b/frontend/src/lib/components/flows/propPicker/PropPickerWrapper.svelte @@ -27,7 +27,7 @@ import type { PickableProperties } from '../previousResults' import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte' import type { PropPickerContext } from '$lib/components/prop_picker' - import type { FlowEditorContext } from '../types' + interface Props { pickableProperties: PickableProperties | undefined @@ -67,9 +67,8 @@ const { flowPropPickerConfig } = getContext('PropPickerContext') flowPropPickerConfig.set(undefined) - const { flowStore } = getContext('FlowEditorContext') - let flow_env = $derived(pickableProperties?.flow_env || flowStore.val.value.flow_env) + setContext('PropPickerWrapper', { propPickerConfig, inputMatches, @@ -156,7 +155,6 @@ {extraResults} {displayContext} {error} - {flow_env} previousId={pickableProperties?.previousId} {pickableProperties} allowCopy={!notSelectable && !$propPickerConfig} diff --git a/frontend/src/lib/components/propertyPicker/PropPicker.svelte b/frontend/src/lib/components/propertyPicker/PropPicker.svelte index d04683e226..6910750480 100644 --- a/frontend/src/lib/components/propertyPicker/PropPicker.svelte +++ b/frontend/src/lib/components/propertyPicker/PropPicker.svelte @@ -19,7 +19,6 @@ error?: boolean allowCopy?: boolean previousId?: string | undefined - flow_env?: Record | undefined result?: any | undefined extraResults?: any } @@ -30,7 +29,6 @@ error = false, allowCopy = false, previousId = undefined, - flow_env = undefined, result = undefined, extraResults = undefined }: Props = $props() @@ -39,7 +37,6 @@ let resources: Record = $state({}) let displayVariable = $state(false) let displayResources = $state(false) - let displayFlowEnv = $state(false) let allResultsCollapsed = $state(true) let collapsableInitialState: @@ -47,7 +44,6 @@ allResultsCollapsed: boolean displayVariable: boolean displayResources: boolean - displayFlowEnv: boolean } | undefined @@ -139,7 +135,9 @@ resultByIdFiltered = {} } if (!$inputMatches?.some((match) => match.word === 'flow_env')) { - flowEnvFiltered = {} + if (search === EMPTY_STRING) { + flowEnvFiltered = pickableProperties.flow_env + } } if ($inputMatches?.length == 1) { filteringFlowInputsOrResult = $inputMatches[0].value @@ -185,8 +183,7 @@ collapsableInitialState = { allResultsCollapsed, displayVariable, - displayResources, - displayFlowEnv + displayResources } } @@ -200,10 +197,6 @@ displayResources = true return } - if ($inputMatches[0].word === 'flow_env') { - displayFlowEnv = true - return - } if ($inputMatches[0].word === 'results') { allResultsCollapsed = false return @@ -214,8 +207,7 @@ if (!collapsableInitialState) { return } - ;({ allResultsCollapsed, displayVariable, displayResources, displayFlowEnv } = - collapsableInitialState) + ;({ allResultsCollapsed, displayVariable, displayResources } = collapsableInitialState) collapsableInitialState = undefined } @@ -279,6 +271,18 @@ /> {/if} + {#if flowEnvFiltered && Object.keys(flowEnvFiltered ?? {}).length > 0} + Flow Env Variables +
+ +
+ {/if} {#if error} Error
@@ -445,45 +449,6 @@ {/if}
{/if} - {#if flow_env && Object.keys(flow_env).length > 0 && $inputMatches?.some((match) => match.word === 'flow_env')} -
- Flow Env Variables: - - {#if displayFlowEnv} - - - {:else} - - {/if} -
- {/if} {/if} diff --git a/frontend/src/routes/(root)/(logged)/variables/+page.svelte b/frontend/src/routes/(root)/(logged)/variables/+page.svelte index 4082e5f8ff..870bf8e7c4 100644 --- a/frontend/src/routes/(root)/(logged)/variables/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/variables/+page.svelte @@ -40,7 +40,8 @@ EyeOff, Circle } from 'lucide-svelte' - import { untrack } from 'svelte' + import { onMount, untrack } from 'svelte' + import { page } from '$app/stores' type ListableVariableW = ListableVariable & { canWrite: boolean } @@ -202,6 +203,14 @@ loadContextualVariables() }, 5000) } + + onMount(() => { + let hash = $page.url.hash + if (hash.length > 1) { + let path = hash.slice(1) + variableEditor?.editVariable(path) + } + }) diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 2b181a6c7b..5031c7e889 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -96,9 +96,8 @@ components: type: boolean flow_env: type: object - description: Environment variables available to all steps - additionalProperties: - type: string + description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)." + additionalProperties: {} priority: type: number description: Execution priority (higher numbers run first)