Skip to content

Work Sources

A Work Source is an external system that provides tasks for agents to work on. herdctl uses an extensible adapter pattern to integrate with various task management systems like GitHub Issues, Linear, Jira, and more.

Work sources answer the question: “What should this agent work on?”

While schedules control when an agent runs, work sources control what the agent works on when it runs.

┌─────────────────────────────────────────────────────────────────────┐
│ WORK SOURCE FLOW │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ External │───▶│ Work │───▶│ Agent │ │
│ │ System │ │ Source │ │ │ │
│ │ │ │ Adapter │ │ Processes the task │ │
│ │ (GitHub, │ │ │ │ and reports outcome │ │
│ │ Linear, │◀───│ Normalizes │◀───│ │ │
│ │ Jira...) │ │ work items │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
│ │
│ Example: GitHub Issues → GitHub Adapter → Agent works on issue │
│ │
└─────────────────────────────────────────────────────────────────────┘

herdctl implements a pluggable adapter pattern that enables:

  • Consistent Interface: All work sources provide the same operations (fetch, claim, complete, release)
  • Source-Specific Logic: Each adapter handles the unique requirements of its platform
  • Easy Extension: New adapters can be added without modifying core code
  • Type Safety: Full TypeScript support for configuration and work items
┌─────────────────────────────────────────────────────────────────────┐
│ ADAPTER ARCHITECTURE │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ WorkSourceAdapter Interface │ │
│ │ │ │
│ │ fetchAvailableWork() → Get tasks ready for processing │ │
│ │ claimWork() → Mark a task as being worked on │ │
│ │ completeWork() → Report task completion/failure │ │
│ │ releaseWork() → Return uncompleted task to queue │ │
│ │ getWork() → Fetch a specific task by ID │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ GitHub │ │ Linear │ │ Jira │ │
│ │ Adapter │ │ Adapter │ │ Adapter │ │
│ │ │ │ │ │ │ │
│ │ Label-based │ │ Status-based │ │ Transition │ │
│ │ workflow │ │ workflow │ │ workflow │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

Regardless of the source, all tasks are normalized into a consistent WorkItem structure:

FieldTypeDescription
idstringUnique identifier (source-prefixed, e.g., github-123)
sourcestringThe adapter type (github, linear, etc.)
externalIdstringOriginal ID in the external system
titlestringTask title or summary
descriptionstringFull task description or body
prioritystringcritical, high, medium, or low
labelsstring[]Tags/labels from the source system
urlstringURL to view in the external system
createdAtDateWhen the task was created
updatedAtDateWhen the task was last modified
metadataobjectSource-specific data (assignees, milestones, etc.)

Every work item follows a standard lifecycle across all adapters:

┌─────────────────────────────────────────────────────────────────────┐
│ WORK ITEM LIFECYCLE │
│ │
│ ┌───────────────┐ │
│ │ AVAILABLE │ │
│ │ Ready label │ │
│ └───────┬───────┘ │
│ │ │
│ claimWork() │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ CLAIMED │ │
│ │ In-progress │ │
│ │ label │ │
│ └───────┬───────┘ │
│ │ │
│ ┌─────────────┴─────────────┐ │
│ │ │ │
│ completeWork() releaseWork() │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ COMPLETED │ │ RELEASED │ │
│ │ (closed │ │ (returned │ │
│ │ if success) │ │ to queue) │ │
│ └───────────────┘ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
OperationPurposeWhat Happens
fetchAvailableWork()Get tasks ready for processingReturns unclaimed tasks matching filters
claimWork(id)Reserve a task for this agentMarks task as in-progress, preventing others from claiming
completeWork(id, result)Report task outcomePosts results, closes task on success
releaseWork(id, options)Return task to queueRemoves in-progress status, optionally re-adds ready status
getWork(id)Fetch a specific taskReturns the task details

The GitHub adapter uses labels to track work item state:

  • Ready Label: Marks issues available for agents (default: ready)
  • In-Progress Label: Applied when an agent claims work (default: agent-working)
  • Exclude Labels: Issues with these labels are skipped (default: blocked, wip)

See the GitHub Issues Configuration Guide for detailed setup instructions.

The following adapters are planned:

AdapterWorkflow TypeStatus
LinearStatus-based (columns/states)Planned
JiraStatus transitionsPlanned
NotionDatabase propertiesPlanned
BeadsTask queuePlanned

Work sources are configured at the agent level:

agents/my-coder.yaml
name: my-coder
description: "Implements features from GitHub issues"
workspace: my-project
repo: myorg/my-project
# Work source configuration
work_source:
type: github
repo: myorg/my-project
labels:
ready: ready-for-dev
in_progress: agent-working
exclude_labels:
- blocked
- needs-design
auth:
token_env: GITHUB_TOKEN
schedules:
issue-check:
type: interval
interval: 5m
prompt: |
Check for ready issues and implement the oldest one.
Create a PR when done.
FieldTypeRequiredDescription
typestringYesAdapter type (github, etc.)
labelsobjectNoLabel configuration (adapter-specific)
exclude_labelsstring[]NoLabels that exclude issues from processing
authobjectNoAuthentication configuration

Work items include a priority field that adapters infer from source-specific signals:

GitHub: Priority is inferred from labels:

PriorityMatching Labels
criticalcritical, p0, urgent
highhigh, p1, important
mediumDefault (no matching labels)
lowlow, p3

Agents can use priority to decide which tasks to work on first.

When multiple agents share a work source, race conditions can occur when two agents try to claim the same task. herdctl handles this gracefully:

  1. Atomic Claiming: The adapter attempts to claim atomically
  2. Failure Detection: If another agent claimed first, the claim returns already_claimed
  3. Automatic Retry: The scheduler can fetch and try the next available task
agents/coder-1.yaml
# Two agents sharing the same work source
name: coder-1
work_source:
type: github
repo: myorg/my-project
labels:
ready: ready
# agents/coder-2.yaml
name: coder-2
work_source:
type: github
repo: myorg/my-project
labels:
ready: ready