Skip to content

API: Add metadata templates to TaskTemplate for source-aware task labeling and external integration #819

@kelos-bot

Description

@kelos-bot

🤖 Kelos Strategist Agent @gjkim42

Summary

Spawned Tasks currently receive only one hardcoded label (kelos.dev/taskspawner) and a few system annotations (kelos.dev/source-kind, kelos.dev/source-number). There is no way for users to propagate source-item metadata (e.g., issue priority labels, author, area labels) onto Task objects as custom labels or annotations. This limits observability, scheduling flexibility, and integration with external tooling.

Problem

When operating Kelos at scale with many TaskSpawners, operators need to answer questions like:

  • "How many tasks are running for priority/critical issues right now?"
  • "What's the average duration of tasks triggered by team-backend vs team-frontend issues?"
  • "Route tasks for size/XL issues to high-memory node pools"
  • "Show me all tasks related to security vulnerabilities"

Today, none of this is possible because spawned Tasks carry no user-controllable metadata derived from the source item. The only workaround is parsing the rendered prompt text, which is fragile and not queryable via kubectl or Prometheus.

Current task creation code (cmd/kelos-spawner/main.go:319-326):

task := &kelosv1alpha1.Task{
    ObjectMeta: metav1.ObjectMeta{
        Name:      taskName,
        Namespace: ts.Namespace,
        Labels: map[string]string{
            "kelos.dev/taskspawner": ts.Name,  // only label
        },
        Annotations: annotations,  // only source-kind, source-number, github-reporting
    },
    // ...
}

Proposal

Add an optional metadata field to TaskTemplate that supports Go text/template rendering (same engine used by promptTemplate and branch) for both labels and annotations:

API change in api/v1alpha1/taskspawner_types.go:

// TaskTemplateMetadata defines templated metadata for spawned Tasks.
type TaskTemplateMetadata struct {
    // Labels are additional labels to set on spawned Tasks.
    // Values support Go text/template variables from the work item.
    // +optional
    Labels map[string]string `json:"labels,omitempty"`

    // Annotations are additional annotations to set on spawned Tasks.
    // Values support Go text/template variables from the work item.
    // +optional
    Annotations map[string]string `json:"annotations,omitempty"`
}

type TaskTemplate struct {
    // Metadata defines additional labels and annotations for spawned Tasks.
    // Label/annotation values support the same Go text/template variables
    // as promptTemplate: {{.ID}}, {{.Title}}, {{.Kind}}, {{.Number}},
    // {{.Labels}}, {{.URL}}, {{.Branch}}, etc.
    // User-defined labels are merged with system labels; system labels
    // (kelos.dev/*) take precedence on conflict.
    // +optional
    Metadata *TaskTemplateMetadata `json:"metadata,omitempty"`

    // ... existing fields
}

Example TaskSpawner config:

apiVersion: kelos.dev/v1alpha1
kind: TaskSpawner
metadata:
  name: issue-worker
spec:
  when:
    githubIssues:
      labels: ["agent-task"]
      priorityLabels: ["priority/critical", "priority/important-soon"]
  taskTemplate:
    metadata:
      labels:
        kelos.dev/source-kind: "{{.Kind}}"
        kelos.dev/priority: "{{index .Labels 0}}"
        team.example.com/area: "backend"
      annotations:
        kelos.dev/source-url: "{{.URL}}"
        kelos.dev/source-title: "{{.Title}}"
    type: claude-code
    credentials:
      type: api-key
      secretRef:
        name: anthropic-key
    workspaceRef:
      name: my-repo
    promptTemplate: |
      Fix the following issue: {{.Title}}
      {{.Body}}

Spawner code change (cmd/kelos-spawner/main.go):

// After rendering prompt and branch, render metadata templates:
taskLabels := map[string]string{
    "kelos.dev/taskspawner": ts.Name,
}
if ts.Spec.TaskTemplate.Metadata != nil {
    for k, v := range ts.Spec.TaskTemplate.Metadata.Labels {
        rendered, err := source.RenderTemplate(v, item)
        if err != nil {
            log.Error(err, "rendering label template", "key", k)
            continue
        }
        if !strings.HasPrefix(k, "kelos.dev/") || taskLabels[k] == "" {
            taskLabels[k] = rendered
        }
    }
}

Use cases enabled

Use case How
Priority-based node scheduling Set kelos.dev/priority label → use with nodeAffinity in podOverrides or a future PriorityClass integration
Prometheus metrics segmentation Query kelos_task_duration_seconds{kelos_dev_priority="critical"} via label-based metric relabeling
kubectl filtering kubectl get tasks -l kelos.dev/priority=critical or kelos get tasks with label selectors
Cost allocation Label tasks with team, project, or cost-center for chargeback via Kubecost/OpenCost
External dashboard integration Grafana/Datadog can read task labels for filtering and grouping
Audit and compliance Annotate tasks with source URLs, authors, and timestamps for traceability
Multi-team routing Different teams label their spawners differently, enabling team-scoped views

Design considerations

  1. Backward compatible: metadata is fully optional; existing TaskSpawners are unaffected
  2. System label precedence: kelos.dev/taskspawner and system annotations always win on conflict, preventing users from breaking internal label contracts
  3. Template rendering reuse: Uses the same source.RenderTemplate() already used for promptTemplate and branch — no new template engine
  4. Kubernetes label validation: Rendered values must pass Kubernetes label/annotation validation; invalid renders should be logged and skipped (not fail the task)
  5. Label key restrictions: Consider forbidding overrides of kelos.dev/taskspawner explicitly to protect the spawner→task ownership link

Relationship to existing issues

/kind api

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions