Context
Workflows currently need verbose reference objects (e.g., @{ ValueFrom = 'Request.Input.UserPrincipalName' }) to pull data from the lifecycle request.
The repository already contains live workflow examples using {{...}} placeholders (e.g., {{Request.Input.DisplayName}}), but the engine currently treats those strings as literals (no resolution during planning). citeturn2view0
Problem statement
Workflow authors want a concise, readable way to embed request values into string fields inside workflow definitions without introducing executable logic (must remain data-only).
Proposed solution (V1)
Add deterministic template resolution during planning (plan build), so the plan artifact contains resolved strings and execution remains “execute what was planned”.
Template syntax
- Placeholder format:
{{<Path>}}
<Path> is a dot-separated property path.
- Placeholders may appear multiple times within one string.
Examples:
IdentityKey = '{{Request.Input.UserPrincipalName}}'
InternalMessage = '{{Request.Input.DisplayName}} is no longer with the organization.'
Where resolution applies
Resolve templates in all string values found in:
Steps[*].With (including nested hashtables / arrays)
OnFailureSteps[*].With (including nested structures)
Not in scope (V1):
- expression evaluation, formatting pipelines, condition evaluation, or any “mini-language”
- resolving templates inside non-string values (e.g., turning
{{...}} into objects)
- runtime (execute-time) resolution — planning-time only
Allowed placeholder roots (security boundary)
To avoid “leaking” trusted execution context or enabling unintended access, template resolution must only allow:
Request.Input.* (preferred “authoring surface” for request payload)
Request.DesiredState.* (supported alias surface for existing request model)
Request.IdentityKeys.*
Request.Changes.*
Request.LifecycleEvent
Request.CorrelationId
Request.Actor
Everything else is rejected with a fail-fast planning error (including Plan.*, Providers.*, Workflow.*).
Request compatibility
Because the current request model in IdLE exposes DesiredState but many examples and step docs use Request.Input.*, the resolver MUST support:
Request.Input.* as:
- the
Input property if it exists on the request object, otherwise
- an alias to
Request.DesiredState.*
This allows existing and future hosts to either:
- provide
Input explicitly, or
- continue using
DesiredState while workflows author against Input.
Resolution rules (deterministic + safe)
Placeholder parsing
- Scan strings for tokens matching
{{...}}.
- Only treat tokens as templates if the content matches a strict path pattern:
^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)*$
- If a string contains
{{ without a matching }}, throw a planning error (typo safety).
Value extraction
- Resolve
<Path> against the request object only (per allowed roots above).
- If the path does not exist, or resolves to
$null:
- fail fast with an actionable error identifying:
- Step name
- placeholder path
- missing segment / root
Rationale: silent substitution to '' commonly creates broken identities (e.g., empty UPN) and is hard to diagnose.
Type handling
- Resolved values must be scalar-ish and convertible to string:
- allowed:
string, numeric, bool, DateTime, Guid (converted via [string])
- rejected: hashtable/dictionary, array/list, PSCustomObject (unless it stringifies safely—V1 rejects to be strict)
If a non-scalar value is resolved, fail fast and instruct the workflow author to use an explicit mapping step or host-side pre-flattening.
Escaping
Allow a literal {{ sequence inside a string via:
\{{ → treated as literal {{ (escape is removed after resolution)
This mitigates the main breaking-change risk (users wanting literal braces).
Security considerations
- No ScriptBlocks are introduced (resolution is string-only).
- Resolver must not call
Invoke-Expression, ExpandString, or similar mechanisms.
- Only allowlisted request roots may be accessed.
- Keep existing “redaction at output boundaries” intact:
- plans/exports/events must still redact secret values by key/type (as already defined in docs/implementation).
Acceptance criteria
- Building a plan resolves templates in
With strings (including nested objects) for both Steps and OnFailureSteps.
- Invalid templates fail planning with deterministic, actionable errors.
- Disallowed roots (e.g.,
{{Providers.AuthSessionBroker}}) fail planning.
Request.Input.* works even if the request object only provides DesiredState (alias behavior).
- Unit tests cover all documented behaviors.
- Documentation is updated to describe template syntax and guidance.
- Existing examples that use templates become executable (planning produces resolved values).
Work breakdown
1) Core implementation
- Add a private resolver function in IdLE.Core (e.g.,
Resolve-IdleWorkflowTemplates / Resolve-IdleTemplateString).
- Apply it during planning when normalizing steps in
New-IdlePlanObject (after copying With, before emitting plan steps).
- Ensure recursion over:
- hashtables/dictionaries
- arrays/lists
- nested PSCustomObjects that appear in workflow data (if any)
2) Tests (Pester)
Add tests under tests/IdLE.Core (or the existing planning test area) for:
- single placeholder substitution
- multiple placeholders in one string
- nested hashtable and nested array substitution
- invalid syntax (unbalanced braces)
- invalid path pattern (spaces, special chars)
- missing path segment
$null resolved value
- disallowed root access
Request.Input alias behavior (Input missing → use DesiredState)
- escaping (
\{{ literal)
3) Docs / examples
- Update
docs/usage/workflows.md with a new section:
- “Template substitution ({{...}})”
- supported roots
- alias rules
- escaping
- troubleshooting errors
- Update step pack READMEs that currently recommend
ValueFrom objects to:
- show templates for string fields, and clarify when templates are (not) appropriate.
- Verify example workflows under
examples/workflows/live are consistent.
Non-goals (explicit)
- No “formatters” like
{{Path | upper}}
- No conditional templates
- No evaluation of PowerShell expressions
- No runtime resolution at execution time
Suggested branch / PR hygiene
- Branch:
issues/107-template-substitution-workflow-config
- Keep commits scoped:
- core resolver + wiring
- tests
- docs/examples
- PR must be green: Pester + PSScriptAnalyzer, and no unrelated refactors.
Context
Workflows currently need verbose reference objects (e.g.,
@{ ValueFrom = 'Request.Input.UserPrincipalName' }) to pull data from the lifecycle request.The repository already contains live workflow examples using
{{...}}placeholders (e.g.,{{Request.Input.DisplayName}}), but the engine currently treats those strings as literals (no resolution during planning). citeturn2view0Problem statement
Workflow authors want a concise, readable way to embed request values into string fields inside workflow definitions without introducing executable logic (must remain data-only).
Proposed solution (V1)
Add deterministic template resolution during planning (plan build), so the plan artifact contains resolved strings and execution remains “execute what was planned”.
Template syntax
{{<Path>}}<Path>is a dot-separated property path.Examples:
Where resolution applies
Resolve templates in all string values found in:
Steps[*].With(including nested hashtables / arrays)OnFailureSteps[*].With(including nested structures)Not in scope (V1):
{{...}}into objects)Allowed placeholder roots (security boundary)
To avoid “leaking” trusted execution context or enabling unintended access, template resolution must only allow:
Request.Input.*(preferred “authoring surface” for request payload)Request.DesiredState.*(supported alias surface for existing request model)Request.IdentityKeys.*Request.Changes.*Request.LifecycleEventRequest.CorrelationIdRequest.ActorEverything else is rejected with a fail-fast planning error (including
Plan.*,Providers.*,Workflow.*).Request compatibility
Because the current request model in IdLE exposes
DesiredStatebut many examples and step docs useRequest.Input.*, the resolver MUST support:Request.Input.*as:Inputproperty if it exists on the request object, otherwiseRequest.DesiredState.*This allows existing and future hosts to either:
Inputexplicitly, orDesiredStatewhile workflows author againstInput.Resolution rules (deterministic + safe)
Placeholder parsing
{{...}}.^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)*${{without a matching}}, throw a planning error (typo safety).Value extraction
<Path>against the request object only (per allowed roots above).$null:Rationale: silent substitution to
''commonly creates broken identities (e.g., empty UPN) and is hard to diagnose.Type handling
string, numeric,bool,DateTime,Guid(converted via[string])If a non-scalar value is resolved, fail fast and instruct the workflow author to use an explicit mapping step or host-side pre-flattening.
Escaping
Allow a literal
{{sequence inside a string via:\{{→ treated as literal{{(escape is removed after resolution)This mitigates the main breaking-change risk (users wanting literal braces).
Security considerations
Invoke-Expression,ExpandString, or similar mechanisms.Acceptance criteria
Withstrings (including nested objects) for bothStepsandOnFailureSteps.{{Providers.AuthSessionBroker}}) fail planning.Request.Input.*works even if the request object only providesDesiredState(alias behavior).Work breakdown
1) Core implementation
Resolve-IdleWorkflowTemplates/Resolve-IdleTemplateString).New-IdlePlanObject(after copyingWith, before emitting plan steps).2) Tests (Pester)
Add tests under
tests/IdLE.Core(or the existing planning test area) for:$nullresolved valueRequest.Inputalias behavior (Inputmissing → useDesiredState)\{{literal)3) Docs / examples
docs/usage/workflows.mdwith a new section:ValueFromobjects to:examples/workflows/liveare consistent.Non-goals (explicit)
{{Path | upper}}Suggested branch / PR hygiene
issues/107-template-substitution-workflow-config