Add template substitution syntax for workflow configurations#108
Add template substitution syntax for workflow configurations#108
Conversation
Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com>
Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com>
Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com>
Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com>
Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce5bcf6063
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This pull request implements template substitution syntax for workflow configurations, allowing workflow authors to use concise {{Path}} placeholders instead of verbose @{ ValueFrom = '...' } reference objects. The feature enables embedding request values directly into string fields at planning time, with strict security boundaries and fail-fast error handling.
Changes:
- Adds
Resolve-IdleTemplateString.ps1to parse and resolve{{...}}placeholders against request context - Adds
Resolve-IdleWorkflowTemplates.ps1to recursively process nested data structures - Integrates template resolution into
New-IdlePlanObject.ps1after copying stepWithconfigurations - Creates 26 test fixture files and comprehensive test coverage (though test file still uses inline definitions)
- Documents template syntax, security boundaries, and usage patterns in
docs/usage/workflows.md
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1 |
Core template parsing and resolution engine with security boundary enforcement and type validation |
src/IdLE.Core/Private/Resolve-IdleWorkflowTemplates.ps1 |
Recursive traversal of hashtables/arrays/objects to resolve template strings throughout step configurations |
src/IdLE.Core/Public/New-IdlePlanObject.ps1 |
Integration point for template resolution, called after With copy and before plan emission |
tests/Resolve-IdleWorkflowTemplates.Tests.ps1 |
Comprehensive test suite covering all template scenarios (26 tests) |
tests/fixtures/workflows/template-tests/*.psd1 |
26 test fixture files for template test scenarios (created but largely unused) |
docs/usage/workflows.md |
Complete documentation of template syntax, security boundary, error handling, and usage guidance |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Replace all Set-Content in tests with fixture file references - Fix brace balance check to handle escaped sequences correctly - Remove duplication in Input property existence check - Reject empty arrays consistently with non-empty arrays - Verify all existing workflow examples still work Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com>
Summary
Adds
{{Path}}template syntax to workflow configurations, resolving placeholders at planning time. Replaces verbose@{ ValueFrom = '...' }objects with concise string templates.Motivation
Current workflows require verbose reference objects for embedding request values:
Template syntax enables readable, inline value substitution:
Type of Change
Changes
Core Resolution Engine
Resolve-IdleTemplateString.ps1: Parses{{Path}}placeholders, validates security boundary, resolves against request context\{{mixed with actual templatesResolve-IdleWorkflowTemplates.ps1: Recursively processes hashtables/arrays/objectsNew-IdlePlanObject.ps1: Integrated resolution afterWithcopy, before plan emissionSecurity Boundary
Allowlisted roots only:
Request.Input.*(aliased toRequest.DesiredState.*when Input property missing)Request.DesiredState.*,Request.IdentityKeys.*,Request.Changes.*Request.LifecycleEvent,Request.CorrelationId,Request.ActorAccess to
Plan.*,Providers.*,Workflow.*rejected at planning time.Error Handling
Fail-fast on:
Features
\{{→ literal{{Documentation
docs/usage/workflows.md: Comprehensive template syntax guide including:New-IdleLifecycleRequestand pass values viaDesiredStateIdLE.Step.EmitEventfor messages,IdLE.Step.CreateIdentitywith properAttributesstructure)Test Organization
tests/fixtures/workflows/template-tests/Get-TemplateTestFixturehelper functionSet-Contentworkflow definitions removed (375 lines eliminated)Compatibility
entraid-joiner-complete.psd1entraid-leaver-offboarding.psd1entraid-mover-department-change.psd1joiner-with-entraid-sync.psd1Testing
How to test & review
Run existing live examples that already use templates:
Check security boundary by attempting disallowed roots in a test workflow:
Checklist
Related Issues
Implements the template substitution specification from issue #107.
Original prompt
This section details on the original issue you should resolve
<issue_title>Template substitution syntax for workflow configurations</issue_title>
<issue_description>## 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).✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.