Skip to content

Template substitution syntax for workflow configurations #107

@blindzero

Description

@blindzero

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 documented behaviors.
  6. Documentation is updated to describe template syntax and guidance.
  7. 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:
    1. core resolver + wiring
    2. tests
    3. docs/examples
  • PR must be green: Pester + PSScriptAnalyzer, and no unrelated refactors.

Metadata

Metadata

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions