API: Add taskCompletion trigger source and structured outputs for multi-step agent workflows
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 331
- Forks
- 40
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 70
Description
🤖 Axon Agent @gjkim42
Summary
Axon's current primitives (Task, Workspace, TaskSpawner) handle single-step autonomous workflows well, but many real-world agent workflows are multi-step: one agent's output feeds into the next agent's input. Today, this requires external orchestration or encoding the entire multi-step workflow into a single monolithic prompt. This proposal adds a taskCompletion trigger source to TaskSpawner and structured key-value outputs to Task, enabling task chaining using existing primitives — no new CRDs needed.
Problem
1. No way to chain Tasks together
The When struct only supports two triggers (api/v1alpha1/taskspawner_types.go:20-29):
type When struct {
GitHubIssues *GitHubIssues `json:"githubIssues,omitempty"`
Cron *Cron `json:"cron,omitempty"`
}
There's no way to say "when Task X finishes, start Task Y with X's results." Users who need multi-step workflows must either:
- Cram everything into one giant prompt (brittle, expensive, hard to debug)
- Build external orchestration outside of Kubernetes (defeats the purpose of Axon)
- Manually create follow-up Tasks
2. Task outputs are unstructured
Task outputs (task_types.go:146) are []string — raw lines captured between markers:
Outputs []string `json:"outputs,omitempty"`
The output parser (internal/controller/output_parser.go) just splits lines between ---AXON_OUTPUTS_START--- and ---AXON_OUTPUTS_END---. There's no way to capture named values that a downstream Task could reference. For example, an agent that creates a PR produces output like https://github.com/org/repo/pull/123 and branch: fix-bug, but a downstream agent can't easily extract and use these values.
Proposed API Changes
Part 1: Structured Key-Value Outputs on Task
Extend TaskStatus with a structured output map alongside the existing outputs list:
type TaskStatus struct {
// ... existing fields ...
// Outputs contains raw output lines produced by the agent.
// +optional
Outputs []string `json:"outputs,omitempty"`
// OutputMap contains structured key-value outputs produced by the agent.
// Parsed from lines in the format "key: value" between output markers.
// +optional
OutputMap map[string]string `json:"outputMap,omitempty"`
}
The output parser already captures lines like branch: fix-bug — it just needs to also parse key: value pairs into OutputMap. Lines that don't match the key: value pattern (like bare URLs) continue to go into Outputs as before.
Changes to internal/controller/output_parser.go:
type ParsedOutputs struct {
Lines []string
KeyValues map[string]string
}
func ParseOutputs(logData string) *ParsedOutputs {
// ... existing marker extraction ...
result := &ParsedOutputs{KeyValues: make(map[string]string)}
for _, line := range lines {
if k, v, ok := parseKeyValue(line); ok {
result.KeyValues[k] = v
}
result.Lines = append(result.Lines, line)
}
return result
}
This is fully backward compatible — existing agents that emit branch: my-branch will automatically get structured output without any changes.
Part 2: taskCompletion Trigger Source on TaskSpawner
Add a new trigger type to When:
type When struct {
GitHubIssues *GitHubIssues `json:"githubIssues,omitempty"`
Cron *Cron `json:"cron,omitempty"`
TaskCompletion *TaskCompletion `json:"taskCompletion,omitempty"` // NEW
}
type TaskCompletion struct {
// Selector matches completed Tasks by label. When a Task matching
// this selector reaches a terminal phase, the spawner creates a
// new downstream Task.
// +kubebuilder:validation:Required
Selector map[string]string `json:"selector"`
// Phase filters which terminal phase triggers spawning.
// Defaults to "Succeeded". Set to "Any" to trigger on both
// Succeeded and Failed.
// +kubebuilder:validation:Enum=Succeeded;Failed;Any
// +kubebuilder:default=Succeeded
// +optional
Phase string `json:"phase,omitempty"`
}
The spawner implementation would use the existing Kubernetes watch API (not polling) to watch for Task status transitions — this is both efficient and low-latency:
type TaskCompletionSource struct {
Client client.Client
Namespace string
Selector map[string]string
Phase string
// tracks already-processed Tasks to avoid duplicates
Seen map[string]bool
}
func (s *TaskCompletionSource) Discover(ctx context.Context) ([]WorkItem, error) {
var tasks axonv1alpha1.TaskList
err := s.Client.List(ctx, &tasks,
client.InNamespace(s.Namespace),
client.MatchingLabels(s.Selector),
)
// Filter to terminal tasks not yet seen...
// Convert to WorkItems with outputs available as template variables
}
Part 3: Template Variables for Completed Tasks
New template variables available in promptTemplate when using taskCompletion:
| Variable | Description |
|---|---|
{{.TaskName}} |
Name of the completed upstream Task |
{{.TaskPhase}} |
Phase of the completed Task (Succeeded or Failed) |
{{.Outputs}} |
Raw output lines (joined with newlines) |
{{.OutputMap}} |
Structured key-value map (access as {{index .OutputMap "branch"}}) |
{{.Prompt}} |
The prompt that was given to the upstream Task |
{{.Labels}} |
Labels from the completed Task |
Use Cases
1. Code + Review Pipeline
Agent A writes code, then Agent B (a different model or specialized reviewer) reviews it:
# Step 1: Code writer
apiVersion: axon.io/v1alpha1
kind: TaskSpawner
metadata:
name: code-writer
spec:
when:
githubIssues:
labels: [bug]
taskTemplate:
type: claude-code
workspaceRef:
name: my-repo
credentials:
type: oauth
secretRef:
name: claude-creds
promptTemplate: |
Fix issue #{{.Number}}: {{.Title}}
{{.Body}}
Create a PR with the fix.
podOverrides:
env:
- name: AXON_STEP
value: "code-writer"
---
# Step 2: Auto-reviewer triggered by code-writer completion
apiVersion: axon.io/v1alpha1
kind: TaskSpawner
metadata:
name: code-reviewer
spec:
when:
taskCompletion:
selector:
axon.io/taskspawner: code-writer
phase: Succeeded
taskTemplate:
type: claude-code
model: opus
workspaceRef:
name: my-repo
credentials:
type: oauth
secretRef:
name: claude-creds
promptTemplate: |
Review the PR created by the upstream agent.
PR URL: {{index .OutputMap "pr_url"}}
Branch: {{index .OutputMap "branch"}}
Check for:
- Security vulnerabilities
- Performance issues
- Test coverage
Leave a thorough review on the PR.
2. Test-Fix Loop
An agent runs tests; if they fail, a fix agent is spawned:
# Step 1: Test runner (cron)
apiVersion: axon.io/v1alpha1
kind: TaskSpawner
metadata:
name: nightly-tests
spec:
when:
cron:
schedule: "0 2 * * *"
taskTemplate:
type: claude-code
workspaceRef:
name: my-repo
credentials:
type: api-key
secretRef:
name: anthropic-key
promptTemplate: |
Run `make test` and report results.
Output the test results summary as structured outputs:
status: pass/fail
failures: <count>
summary: <one-line summary>
---
# Step 2: Fix agent, only when tests fail
apiVersion: axon.io/v1alpha1
kind: TaskSpawner
metadata:
name: test-fixer
spec:
when:
taskCompletion:
selector:
axon.io/taskspawner: nightly-tests
phase: Any # trigger on both success and failure
taskTemplate:
type: claude-code
model: opus
workspaceRef:
name: my-repo
credentials:
type: oauth
secretRef:
name: claude-creds
promptTemplate: |
{{if eq .TaskPhase "Failed"}}
The nightly test run failed. Here are the results:
{{.Outputs}}
Please investigate and fix the failing tests, then open a PR.
{{else}}
Tests passed. No action needed.
{{end}}
3. Multi-Agent Specialization
Different agents handle different aspects of the same issue — one for backend, one for frontend:
# Backend agent
apiVersion: axon.io/v1alpha1
kind: TaskSpawner
metadata:
name: backend-agent
spec:
when:
githubIssues:
labels: [fullstack]
taskTemplate:
type: claude-code
workspaceRef:
name: my-repo
credentials:
type: oauth
secretRef:
name: claude-creds
promptTemplate: |
Implement the backend changes for issue #{{.Number}}: {{.Title}}
{{.Body}}
Only modify files in cmd/, internal/, api/.
Output the branch name and a summary of API changes.
---
# Frontend agent, triggered when backend is done
apiVersion: axon.io/v1alpha1
kind: TaskSpawner
metadata:
name: frontend-agent
spec:
when:
taskCompletion:
selector:
axon.io/taskspawner: backend-agent
phase: Succeeded
taskTemplate:
type: claude-code
workspaceRef:
name: my-repo
credentials:
type: oauth
secretRef:
name: claude-creds
promptTemplate: |
The backend agent completed work on branch: {{index .OutputMap "branch"}}
API changes summary: {{index .OutputMap "summary"}}
Now implement the frontend changes to use the new API.
Work on top of the same branch.
Implementation Plan
This can be implemented incrementally:
Phase 1: Structured Outputs (small, self-contained)
- Extend
ParseOutputsto returnParsedOutputswith both lines and key-value map - Add
OutputMaptoTaskStatus - Update
task_controller.goto populateOutputMap - Update CRD generation (
make update) - Estimated scope: ~50 lines of code changes + tests
Phase 2: taskCompletion Trigger Source
- Add
TaskCompletiontoWhenstruct - Implement
TaskCompletionSourceininternal/source/ - Extend spawner to handle
taskCompletion(use Kubernetes List with label selector in poll loop, or informer/watch for lower latency) - Add new template variables for completed task data
- Add XValidation rule for mutual exclusivity
- Estimated scope: ~200 lines + tests
Phase 3: CLI Support
axon create taskspawner --source task-completion --selector "axon.io/taskspawner=code-writer"- Add TaskCompletion display to
axon get taskspawner
Design Decisions
Why not a new TaskPipeline CRD?
A new CRD would provide a more "complete" solution for DAG-based workflows, but:
- Composability: TaskSpawner triggers are composable — you can chain any combination of GitHub issues → task completion → cron without a rigid pipeline abstraction
- Simplicity: A trigger on
Whenis a much smaller API surface than a full pipeline CRD with step definitions, conditional branching, and failure handling - Incremental adoption: Users can add one
taskCompletionspawner at a time rather than rewriting their workflow into a pipeline spec - Consistency: The TaskSpawner already handles "watch something → create Tasks" — task completion is just another thing to watch
If complex DAG workflows become common, a TaskPipeline CRD could be introduced later as syntactic sugar that generates the underlying TaskSpawners.
Why label selectors (not direct Task references)?
Label selectors (selector: {axon.io/taskspawner: code-writer}) are more flexible than pointing to a specific upstream Task name:
- TaskSpawner already labels its Tasks with
axon.io/taskspawner: <name>(seecmd/axon-spawner/main.go:180) - Users can add custom labels for more sophisticated routing
- Works naturally with the existing Kubernetes pattern
Polling vs. Watch for taskCompletion
The spawner already uses a poll loop. For task completion:
- Polling is simplest (list Tasks with label selector each cycle) and consistent with existing architecture
- Watch/informer provides lower latency but adds complexity to the spawner
- Recommend starting with polling and migrating to watch later (the
Sourceinterface abstracts this)
Backward Compatibility
- Adding
TaskCompletion *TaskCompletiontoWhenis a non-breaking additive change - Adding
OutputMap map[string]stringtoTaskStatusis a non-breaking additive change - Existing output lines like
branch: my-branchwill be parsed into bothOutputs(as before) andOutputMap(new) - No changes to existing agent images required — they already emit
key: valuelines
References
Whenstruct:api/v1alpha1/taskspawner_types.go:20-29TaskStatus.Outputs:api/v1alpha1/task_types.go:146- Output parser:
internal/controller/output_parser.go - Spawner task creation with labels:
cmd/axon-spawner/main.go:176-193 - Source interface:
internal/source/source.go:20-22 - Prompt rendering:
internal/source/prompt.go - Related: #268 (webhook trigger — different scope, complementary)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with api/v1alpha1/taskspawner_types.go and task_types.go, then read internal/controller/output_parser.go and task_controller.go for the existing output flow. Review internal/source/ and cmd/axon-spawner/main.go for trigger discovery and task labels. Done means structured outputs, taskCompletion triggering, template variables, CRD updates, CLI support, and tests cover the proposed phases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, kubernetes
- Domain
- ai, backend, infrastructure
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100