Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions docs/LANGUAGE-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ The following block types are defined. Tables are auto-generated from [`workflow

### `output "name" { ... }`

- **Source:** [`workflow/schema.go:322`](../workflow/schema.go#L322)
- **Source:** [`workflow/schema.go:320`](../workflow/schema.go#L320)
- **Labels:** `name`
- **Attributes:**

Expand Down Expand Up @@ -159,7 +159,7 @@ The following block types are defined. Tables are auto-generated from [`workflow

### `subworkflow "name" { ... }`

- **Source:** [`workflow/schema.go:332`](../workflow/schema.go#L332)
- **Source:** [`workflow/schema.go:330`](../workflow/schema.go#L330)
- **Labels:** `name`
- **Attributes:**

Expand All @@ -180,17 +180,16 @@ The following block types are defined. Tables are auto-generated from [`workflow
|---|---|---|---|
| `on_crash` | string | no | _(no description)_ |
| `on_failure` | string | no | OnFailure controls iteration failure behaviour: "continue" (default for sequential for_each/count steps), "abort" (stop on first failure; default for parallel steps), or "ignore" (treat all as success). |
| `max_visits` | number | no | MaxVisits limits how many times this step may be evaluated in a single run. 0 (default) means unlimited. Negative values are rejected at compile time. |
| `config` | map(string) | no | Config is the legacy map attribute; retained for parse-time detection so the compiler can emit a helpful "use input { } block" diagnostic. |
| `timeout` | string | no | _(no description)_ |
| `allow_tools` | list(string) | no | _(no description)_ |

- **Additional attributes:** Captures: target (required — adapter traversal e.g. adapter.copilot.main, or subworkflow.<name>); for_each, count, parallel, while (optional iteration controls); environment (optional, bare traversal e.g. shell.ci).
- **Additional attributes:** Captures: target (required — adapter traversal e.g. adapter.copilot.main, or subworkflow.<name>); for_each, count, parallel, while (optional iteration controls); environment (optional, bare traversal e.g. shell.ci); max_visits (optional integer, accepts a literal or a compile-time-resolvable variable/local reference).
- **Nested blocks:** [`input`](#input---), [`secret_input`](#secret_input---), [`outcome`](#outcome-name---)

### `state "name" { ... }`

- **Source:** [`workflow/schema.go:426`](../workflow/schema.go#L426)
- **Source:** [`workflow/schema.go:424`](../workflow/schema.go#L424)
- **Labels:** `name`
- **Attributes:**

Expand All @@ -203,7 +202,7 @@ The following block types are defined. Tables are auto-generated from [`workflow

### `wait "name" { ... }`

- **Source:** [`workflow/schema.go:409`](../workflow/schema.go#L409)
- **Source:** [`workflow/schema.go:407`](../workflow/schema.go#L407)
- **Labels:** `name`
- **Attributes:**

Expand All @@ -216,7 +215,7 @@ The following block types are defined. Tables are auto-generated from [`workflow

### `approval "name" { ... }`

- **Source:** [`workflow/schema.go:418`](../workflow/schema.go#L418)
- **Source:** [`workflow/schema.go:416`](../workflow/schema.go#L416)
- **Labels:** `name`
- **Attributes:**

Expand All @@ -229,13 +228,13 @@ The following block types are defined. Tables are auto-generated from [`workflow

### `switch "name" { ... }`

- **Source:** [`workflow/schema.go:437`](../workflow/schema.go#L437)
- **Source:** [`workflow/schema.go:435`](../workflow/schema.go#L435)
- **Labels:** `name`
- **Nested blocks:** [`match`](#match---), [`default`](#default---)

### `permissions { ... }`

- **Source:** [`workflow/schema.go:476`](../workflow/schema.go#L476)
- **Source:** [`workflow/schema.go:474`](../workflow/schema.go#L474)
- **Attributes:**

| Attribute | Type | Required | Description |
Expand All @@ -245,7 +244,7 @@ The following block types are defined. Tables are auto-generated from [`workflow

### `policy { ... }`

- **Source:** [`workflow/schema.go:457`](../workflow/schema.go#L457)
- **Source:** [`workflow/schema.go:455`](../workflow/schema.go#L455)
- **Attributes:**

| Attribute | Type | Required | Description |
Expand Down Expand Up @@ -273,7 +272,7 @@ The following block types are defined. Tables are auto-generated from [`workflow

### `outcome "name" { ... }`

- **Source:** [`workflow/schema.go:387`](../workflow/schema.go#L387)
- **Source:** [`workflow/schema.go:385`](../workflow/schema.go#L385)
- **Labels:** `name`
- **Attributes:**

Expand All @@ -286,17 +285,17 @@ The following block types are defined. Tables are auto-generated from [`workflow

### `match { ... }`

- **Source:** [`workflow/schema.go:446`](../workflow/schema.go#L446)
- **Source:** [`workflow/schema.go:444`](../workflow/schema.go#L444)
- **Additional attributes:** captures: condition (required), next (required), output (optional)

### `default { ... }`

- **Source:** [`workflow/schema.go:452`](../workflow/schema.go#L452)
- **Source:** [`workflow/schema.go:450`](../workflow/schema.go#L450)
- **Additional attributes:** captures: next (required), output (optional)

### `write { ... }`

- **Source:** [`workflow/schema.go:395`](../workflow/schema.go#L395)
- **Source:** [`workflow/schema.go:393`](../workflow/schema.go#L393)
- **Attributes:**

| Attribute | Type | Required | Description |
Expand Down
11 changes: 5 additions & 6 deletions workflow/compile_steps_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ func compileAdapterStep(g *FSMGraph, sp *StepSpec, spec *Spec, schemas map[strin
timeout, d := decodeStepTimeout(sp)
diags = append(diags, d...)

if sp.MaxVisits < 0 {
diags = append(diags, &hcl.Diagnostic{Severity: hcl.DiagError, Summary: fmt.Sprintf("step %q: max_visits must be >= 0", sp.Name)})
}
maxVisits, d := decodeMaxVisits(sp.Name, sp.Remain, g)
diags = append(diags, d...)

envKey, d := resolveStepEnvironmentOverride(sp.Name, sp.Remain, g)
diags = append(diags, d...)
Expand All @@ -84,7 +83,7 @@ func compileAdapterStep(g *FSMGraph, sp *StepSpec, spec *Spec, schemas map[strin

outputSchema := resolveOutputSchema(adapterType, schemas)

node := newAdapterStepNode(sp, spec, adapterRef, effectiveOnCrash, envKey, timeout, inputMap, inputExprs, secretInputMap, secretInputExprs, outputSchema)
node := newAdapterStepNode(sp, spec, adapterRef, effectiveOnCrash, envKey, timeout, inputMap, inputExprs, secretInputMap, secretInputExprs, outputSchema, maxVisits)
diags = append(diags, maybeCopilotAliasWarnings(sp.Name, adapterType, node.AllowTools)...)
diags = append(diags, compileOutcomeBlock(sp, node, g, opts, schemas[adapterRef].OutputSchema)...)

Expand Down Expand Up @@ -150,14 +149,14 @@ func maybeCopilotAliasWarnings(stepName, adapterName string, tools []string) hcl
func newAdapterStepNode(sp *StepSpec, spec *Spec, adapterRef string, effectiveOnCrash string, envKey string, timeout time.Duration,
inputMap map[string]string, inputExprs map[string]hcl.Expression,
secretInputMap map[string]string, secretInputExprs map[string]hcl.Expression,
outputSchema map[string]ConfigField) *StepNode {
outputSchema map[string]ConfigField, maxVisits int) *StepNode {
return &StepNode{
Name: sp.Name,
TargetKind: StepTargetAdapter,
AdapterRef: adapterRef,
OnCrash: effectiveOnCrash,
OnFailure: sp.OnFailure,
MaxVisits: sp.MaxVisits,
MaxVisits: maxVisits,
Input: inputMap,
InputExprs: inputExprs,
SecretInputs: secretInputMap,
Expand Down
40 changes: 33 additions & 7 deletions workflow/compile_steps_iteration.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ func compileIteratingStep(g *FSMGraph, sp *StepSpec, spec *Spec, schemas map[str
timeout, d := decodeStepTimeout(sp)
diags = append(diags, d...)

if sp.MaxVisits < 0 {
diags = append(diags, &hcl.Diagnostic{Severity: hcl.DiagError, Summary: fmt.Sprintf("step %q: max_visits must be >= 0", sp.Name)})
}
maxVisits, d := decodeMaxVisits(sp.Name, sp.Remain, g)
diags = append(diags, d...)

ie, d := decodeRemainIter(sp, g)
diags = append(diags, d...)
Expand All @@ -71,7 +70,7 @@ func compileIteratingStep(g *FSMGraph, sp *StepSpec, spec *Spec, schemas map[str
if ie.While == nil {
diags = append(diags, validateWhileRefs(sp.Name, swInputExprs)...)
}
node = newSubworkflowIterStepNode(sp, spec, subworkflowRef, effectiveOnCrash, envKey, timeout, swInputExprs)
node = newSubworkflowIterStepNode(sp, spec, subworkflowRef, effectiveOnCrash, envKey, timeout, swInputExprs, maxVisits)
} else {
inputMap, inputExprs, d := decodeStepInput(g, sp, schemas, opts, adapterType)
diags = append(diags, d...)
Expand All @@ -88,7 +87,7 @@ func compileIteratingStep(g *FSMGraph, sp *StepSpec, spec *Spec, schemas map[str
}
}
// each.* references are valid inside iterating steps; no error emitted.
node = newAdapterStepNode(sp, spec, adapterRef, effectiveOnCrash, envKey, timeout, inputMap, inputExprs, secretInputMap, secretInputExprs, outputSchema)
node = newAdapterStepNode(sp, spec, adapterRef, effectiveOnCrash, envKey, timeout, inputMap, inputExprs, secretInputMap, secretInputExprs, outputSchema, maxVisits)
diags = append(diags, maybeCopilotAliasWarnings(sp.Name, adapterType, node.AllowTools)...)
// parallel_safe capability gate: when the step uses parallel = [...] the
// adapter must declare "parallel_safe". When the adapter is absent from the
Expand Down Expand Up @@ -126,14 +125,14 @@ func compileIteratingStep(g *FSMGraph, sp *StepSpec, spec *Spec, schemas map[str
}

// newSubworkflowIterStepNode constructs a StepNode for an iterating subworkflow step.
func newSubworkflowIterStepNode(sp *StepSpec, _ *Spec, subworkflowRef, effectiveOnCrash, envKey string, timeout time.Duration, inputExprs map[string]hcl.Expression) *StepNode {
func newSubworkflowIterStepNode(sp *StepSpec, _ *Spec, subworkflowRef, effectiveOnCrash, envKey string, timeout time.Duration, inputExprs map[string]hcl.Expression, maxVisits int) *StepNode {
return &StepNode{
Name: sp.Name,
TargetKind: StepTargetSubworkflow,
SubworkflowRef: subworkflowRef,
OnCrash: effectiveOnCrash,
OnFailure: sp.OnFailure,
MaxVisits: sp.MaxVisits,
MaxVisits: maxVisits,
Timeout: timeout,
InputExprs: inputExprs,
Outcomes: map[string]*CompiledOutcome{},
Expand Down Expand Up @@ -240,6 +239,33 @@ func decodeParallelMax(stepName string, attr *hcl.Attribute, g *FSMGraph) (int,
return val, nil
}

// decodeMaxVisits decodes and validates the max_visits attribute from a step body.
// It accepts literal integers and compile-time-resolvable references (var.* with a
// known default, local.*). Missing attribute returns 0 (unlimited). Runtime-only
// references and unknown values are rejected with clear diagnostics.
func decodeMaxVisits(stepName string, body hcl.Body, g *FSMGraph) (int, hcl.Diagnostics) {
if body == nil {
return 0, nil
}
attr, diags := readStepBodyAttr("max_visits", body)
if attr == nil {
return 0, diags
}
var val int
d := decodeIntAttr(attr, g, &val)
if d.HasErrors() {
return 0, append(diags, d...)
}
if val < 0 {
return 0, append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Subject: attr.Expr.StartRange().Ptr(),
Summary: fmt.Sprintf("step %q: max_visits must be >= 0", stepName),
})
}
return val, diags
}

// validateWhileExprType performs a compile-time static type check on the while
// expression. If the expression can be folded at compile time (i.e. it references
// only known constants or compile-time-resolved variables) and the result is not
Expand Down
Loading
Loading