Composite Tools Quick Reference
April 21, 2026 · View on GitHub
Quick reference for Virtual MCP Composite Tool workflows.
Basic Workflow Structure
apiVersion: toolhive.stacklok.dev/v1beta1
kind: VirtualMCPCompositeToolDefinition
metadata:
name: my-workflow
namespace: default
spec:
name: my_workflow_name # Tool name exposed to clients
description: What it does # Required description
timeout: 30m # Optional: workflow timeout (default: 30m)
failureMode: abort # Optional: abort|continue (default: abort)
parameters: # Optional: input parameters
param_name:
type: string
description: Description of the parameter
required: false
steps: # Required: workflow steps
- id: step1
type: tool # tool|elicitation|forEach
tool: workload.tool_name
arguments:
key: "{{.params.param_name}}"
Parallel Execution
# Independent steps run in parallel automatically
steps:
- id: fetch_a # Level 1: Runs in parallel ─┐
- id: fetch_b # Level 1: Runs in parallel ─┼─> aggregate
- id: fetch_c # Level 1: Runs in parallel ─┘
- id: aggregate # Level 2: Waits for Level 1
dependsOn: [fetch_a, fetch_b, fetch_c]
Step Dependencies
steps:
- id: step1
- id: step2
dependsOn: [step1] # Runs after step1 completes
- id: step3
dependsOn: [step1, step2] # Waits for both step1 AND step2
Template Syntax
Workflows use Go's text/template syntax with additional context variables and functions.
Basic Access
# Access input parameters
"{{.params.parameter_name}}"
# Access step outputs
"{{.steps.step_id.output}}"
"{{.steps.step_id.output.field_name}}"
"{{.steps.step_id.status}}" # completed|failed|skipped|running
# Access workflow-scoped variables
"{{.vars.variable_name}}"
# Access step errors
"{{.steps.step_id.error}}"
Functions
Composite Tools supports all the built-in functions from the text/template library in addition to some functions for converting to/from JSON.
# JSON encoding - convert value to JSON string
arguments:
data: "{{json .steps.step1.output}}"
# JSON decoding - parse JSON string to access fields
# Useful when MCP servers return JSON as text content
arguments:
name: "{{(fromJson .steps.api.output.text).user.name}}"
# String quoting
arguments:
quoted: "{{quote .params.value}}"
Conditional Logic
# Comparison operators (eq, ne, lt, le, gt, ge)
condition: "{{eq .steps.step1.status \"completed\"}}"
condition: "{{ne .steps.step1.status \"failed\"}}"
condition: "{{gt .steps.step1.output.count 10}}"
# Boolean operators (and, or, not)
condition: "{{and .params.enabled (eq .steps.step1.status \"completed\")}}"
condition: "{{or .params.force (gt .steps.check.output.count 0)}}"
condition: "{{not .params.disabled}}"
Advanced Features
All Go template built-ins are available: index, len, range, with, printf, etc. See Go text/template documentation for complete reference.
Error Handling
Workflow-Level
spec:
failureMode: abort # Stop on first error (default)
failureMode: continue # Log errors, continue workflow
Step-Level (Overrides Workflow)
steps:
# Abort on error (default)
- id: critical
tool: payment.charge
# Uses workflow failureMode
# Continue despite errors
- id: optional
tool: notification.send
onError:
action: continue
# Retry with exponential backoff
- id: resilient
tool: external.api
onError:
action: retry
maxRetries: 3 # Max 3 retries (4 total attempts)
Default Results
Provide fallback values when a step may be skipped (condition) or fail (continue-on-error):
steps:
- id: optional_step
tool: enrichment.api
condition: "{{.params.enable_enrichment}}"
defaultResults:
text: "fallback value" # Used when step is skipped
- id: unreliable_step
tool: external.api
onError:
action: continue
defaultResults:
text: "{\"status\": \"unavailable\"}" # Used when step fails
Notes:
- Keys in
defaultResultsmust match output fields referenced by downstream templates - Backend tools return text under
textkey, so usedefaultResults.textfor text output - Required when skippable steps are referenced by downstream templates
Timeouts
spec:
timeout: 30m # Workflow timeout (default: 30m)
steps:
- id: step1
timeout: 5m # Step timeout (default: 5m)
Precedence: Step timeout ≤ Workflow timeout
Common Patterns
Fan-Out / Fan-In
steps:
# Fan-out: Parallel collection
- id: fetch_1
- id: fetch_2
- id: fetch_3
# Fan-in: Aggregate
- id: combine
dependsOn: [fetch_1, fetch_2, fetch_3]
Sequential Pipeline
steps:
- id: fetch
- id: transform
dependsOn: [fetch]
- id: store
dependsOn: [transform]
Diamond Pattern
steps:
- id: fetch
- id: process_a
dependsOn: [fetch]
- id: process_b
dependsOn: [fetch]
- id: merge
dependsOn: [process_a, process_b]
ForEach Iteration
steps:
- id: get_packages
type: tool
tool: oci.get_image_config
arguments:
image: "{{.params.image}}"
- id: check_vulns
type: forEach
collection: "{{json .steps.get_packages.output.packages}}"
itemVar: pkg # defaults to "item"
maxParallel: 5 # defaults to DAG maxParallel (10)
step: # single inner step (tool only)
type: tool
tool: osv.query_vulnerability
arguments:
package_name: "{{.forEach.pkg.name}}"
dependsOn: [get_packages]
onError:
action: continue # skip failed items, don't abort
Output: {{.steps.check_vulns.output.iterations}}, .count, .completed, .failed
Retry with Fallback
steps:
- id: try_primary
tool: primary.api
onError:
action: retry
maxRetries: 2
- id: use_fallback
tool: secondary.api
dependsOn: [try_primary]
condition: "{{ne .steps.try_primary.status \"completed\"}}"
Validation Rules
- ✅ Workflow name:
^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$(1-64 chars) - ✅ Step IDs must be unique
- ✅ All
dependsOnstep IDs must exist - ✅ No circular dependencies
- ✅ Tool format:
workload_id.tool_name - ✅ Max retry count: 10 (runtime capped - values > 10 are silently reduced with warning)
- ✅ Max workflow steps: 100 (runtime enforced - workflows > 100 steps fail validation)
- ✅ forEach maxIterations: 1000 (hard cap), defaults to 100
- ✅ forEach maxParallel: 50 (hard cap), defaults to DAG maxParallel (10)
- ✅ forEach inner step must be type
tool(no nested forEach or elicitation) - ✅ forEach
itemVarcannot beindex(reserved)
Note: Max retry and max steps limits are currently enforced at runtime. Future work may add CRD-level validation (+kubebuilder:validation:MaxItems=100) and webhook validation to fail at submission time rather than execution time.
Debugging
Check Workflow Status
# In VirtualMCPCompositeToolDefinition
status:
validationStatus: Valid|Invalid
validationErrors:
- "error message here"
referencedBy:
- namespace: default
name: vmcp-server-1
Common Issues
| Error | Cause | Fix |
|---|---|---|
| "output not found" | Missing dependsOn | Add dependency |
| "circular dependency" | Cycle in dependsOn | Remove cycle |
| "tool not found" | Invalid tool reference | Check workload.tool format |
| "template error" | Invalid Go template | Fix template syntax |
Performance Tips
- ✅ Remove unnecessary
dependsOnconstraints - ✅ Group related steps in same execution level
- ✅ Set realistic timeouts based on SLAs
- ✅ Use retry for transient failures only
- ✅ Keep steps focused (one responsibility)