Skip to content

Add template substitution syntax for workflow configurations#108

Merged
blindzero merged 8 commits intomainfrom
copilot/add-template-substitution-syntax
Jan 24, 2026
Merged

Add template substitution syntax for workflow configurations#108
blindzero merged 8 commits intomainfrom
copilot/add-template-substitution-syntax

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 24, 2026

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:

With = @{
  UserPrincipalName = @{ ValueFrom = 'Request.Input.UserPrincipalName' }
  Message = 'Creating user...'  # No dynamic value embedding possible
}

Template syntax enables readable, inline value substitution:

With = @{
  UserPrincipalName = '{{Request.Input.UserPrincipalName}}'
  Message = 'Creating {{Request.Input.DisplayName}} ({{Request.Input.UserPrincipalName}})'
}

Type of Change

  • New feature

Changes

Core Resolution Engine

  • Resolve-IdleTemplateString.ps1: Parses {{Path}} placeholders, validates security boundary, resolves against request context
    • Robust brace balance validation using template pattern matching to correctly handle escaped sequences like \{{ mixed with actual templates
    • Consolidated Input property existence check to eliminate code duplication
    • Consistent array type rejection (empty and non-empty arrays both rejected as non-scalar values)
  • Resolve-IdleWorkflowTemplates.ps1: Recursively processes hashtables/arrays/objects
  • New-IdlePlanObject.ps1: Integrated resolution after With copy, before plan emission

Security Boundary

Allowlisted roots only:

  • Request.Input.* (aliased to Request.DesiredState.* when Input property missing)
  • Request.DesiredState.*, Request.IdentityKeys.*, Request.Changes.*
  • Request.LifecycleEvent, Request.CorrelationId, Request.Actor

Access to Plan.*, Providers.*, Workflow.* rejected at planning time.

Error Handling

Fail-fast on:

  • Missing/null paths
  • Invalid syntax (unbalanced braces, invalid characters)
  • Disallowed roots
  • Non-scalar values (hashtable/array/object resolved from template, including empty arrays)

Features

  • Multiple placeholders per string
  • Nested structure support (hashtables, arrays)
  • Escape sequences: \{{ → literal {{
  • Scalar type coercion (numeric, bool, datetime, guid → string)

Documentation

  • docs/usage/workflows.md: Comprehensive template syntax guide including:
    • "How it works" section explaining the request → template resolution flow
    • Complete example showing how to create requests with New-IdleLifecycleRequest and pass values via DesiredState
    • Corrected examples using appropriate step types (IdLE.Step.EmitEvent for messages, IdLE.Step.CreateIdentity with proper Attributes structure)
    • Allowed roots, error handling, and Request.Input aliasing behavior

Test Organization

  • 26 test fixture files created under tests/fixtures/workflows/template-tests/
  • Test file fully refactored to use Get-TemplateTestFixture helper function
  • All 26 inline Set-Content workflow definitions removed (375 lines eliminated)
  • Tests now consistently load from dedicated fixture files for better maintainability and reusability

Compatibility

  • All 4 existing workflow examples using templates verified working:
    • entraid-joiner-complete.psd1
    • entraid-leaver-offboarding.psd1
    • entraid-mover-department-change.psd1
    • joiner-with-entraid-sync.psd1
  • No breaking changes to existing workflows

Testing

  • Unit tests (26 new tests covering all template scenarios)
  • Manual testing (live example workflows with templates)

How to test & review

Run existing live examples that already use templates:

. ./tools/import-idle.ps1
$wfPath = './examples/workflows/live/entraid-joiner-complete.psd1'
$req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' -DesiredState @{
    UserPrincipalName = 'test@example.com'
    DisplayName = 'Test User'
    # ... other required fields
}
$plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers
# Verify plan.Steps[*].With contains resolved values (not template strings)

Check security boundary by attempting disallowed roots in a test workflow:

With = @{ Value = '{{Plan.WorkflowName}}' }  # Should fail during planning

Checklist

  • Code follows STYLEGUIDE.md
  • Tests added or updated
  • Documentation updated
  • No UI/auth logic added to IdLE.Core
  • No breaking changes without discussion

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). citeturn2view0

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

  1. Building a plan resolves templates in With strings (including nested objects) for both Steps and OnFailureSteps.
  2. Invalid templates fail planning with deterministic, actionable errors.
  3. Disallowed roots (e.g., {{Providers.AuthSessionBroker}}) fail planning.
  4. Request.Input.* works even if the request object only provides DesiredState (alias behavior).
  5. Unit tests cover all d...

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits January 24, 2026 22:15
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>
Copilot AI changed the title [WIP] Add template substitution syntax for workflow configurations Add template substitution syntax for workflow configurations Jan 24, 2026
Copilot AI requested a review from blindzero January 24, 2026 22:20
Copilot AI and others added 2 commits January 24, 2026 22:25
Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com>
Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com>
Copilot AI requested a review from blindzero January 24, 2026 22:29
@blindzero blindzero marked this pull request as ready for review January 24, 2026 22:32
Copilot AI review requested due to automatic review settings January 24, 2026 22:32
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.ps1 to parse and resolve {{...}} placeholders against request context
  • Adds Resolve-IdleWorkflowTemplates.ps1 to recursively process nested data structures
  • Integrates template resolution into New-IdlePlanObject.ps1 after copying step With configurations
  • 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>
@blindzero blindzero merged commit fa1835d into main Jan 24, 2026
6 checks passed
@blindzero blindzero deleted the copilot/add-template-substitution-syntax branch January 24, 2026 23:06
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.

Template substitution syntax for workflow configurations

3 participants