Skip to content

Ticketing Backend Interface

Interface version: 1

All ticketing backends must implement the Handshake RPC with interface_version: 1.

Overview

The ticketing backend is the primary input source for RoboDev. It polls an issue tracker for tickets ready to be processed, manages their lifecycle state transitions (in-progress, complete, failed), and posts progress comments. The controller calls PollReadyTickets on every reconciliation cycle to discover new work.

Interface Summary

Property Value
Proto definition proto/ticketing.proto
Go interface pkg/plugin/ticketing/ticketing.go
Interface version 1
Role in lifecycle Entry point — the controller calls PollReadyTickets every poll interval
Criticality Critical — the controller cannot operate without a ticketing backend

Go Interface

type Backend interface {
    PollReadyTickets(ctx context.Context) ([]Ticket, error)
    MarkInProgress(ctx context.Context, ticketID string) error
    MarkComplete(ctx context.Context, ticketID string, result engine.TaskResult) error
    MarkFailed(ctx context.Context, ticketID string, reason string) error
    AddComment(ctx context.Context, ticketID string, comment string) error
    Name() string
    InterfaceVersion() int
}

Ticket Struct

The Ticket struct represents a unit of work from an external issue tracker:

type Ticket struct {
    ID          string            // Unique identifier (e.g. issue number or Jira key).
    Title       string            // Short summary of the task.
    Description string            // Full task description (used as agent prompt input).
    TicketType  string            // Task category (e.g. "bug", "enhancement", "refactor").
    Labels      []string          // Labels or tags from the source system.
    RepoURL     string            // Repository the agent should work on.
    ExternalURL string            // Link back to the original issue for humans.
    Raw         map[string]any    // Unstructured data from the source system.
}

The Raw field allows backends to pass through arbitrary metadata from the source system. Engines and other plugins can inspect Raw for backend-specific fields (e.g. Jira priority, custom fields).

RPC Methods

Handshake

Version negotiation called once at plugin startup. The controller verifies that the plugin implements a compatible interface version before using it.

rpc Handshake(HandshakeRequest) returns (HandshakeResponse);

PollReadyTickets

Retrieves tickets that are ready for processing. The controller calls this on every reconciliation cycle (default: every 30 seconds).

rpc PollReadyTickets(PollReadyTicketsRequest) returns (PollReadyTicketsResponse);

message PollReadyTicketsRequest {
  repeated string labels = 1;    // Filter tickets by these labels.
  int32 max_results = 2;         // Maximum number of tickets to return.
}

message PollReadyTicketsResponse {
  repeated Ticket tickets = 1;
}

Implementation guidance:

  • Return only tickets in a "ready" state (e.g., open issues with the robodev label that have not already been picked up).
  • Respect max_results to avoid overwhelming the controller when there is a large backlog.
  • The controller handles deduplication via idempotency keys, but filtering already-in-progress tickets at the source is more efficient and reduces unnecessary API calls.
  • Consider caching the last poll result to avoid redundant API requests when the source system has not changed.

MarkInProgress

Transitions a ticket to in-progress state. Called after the controller creates a TaskRun and launches a K8s Job.

rpc MarkInProgress(MarkInProgressRequest) returns (MarkInProgressResponse);

message MarkInProgressRequest {
  string ticket_id = 1;
}

Implementation guidance:

  • Typically this adds a label (e.g., in-progress) or moves the ticket to a "doing" column/status.
  • This operation must be idempotent — calling it twice for the same ticket must not fail or produce side effects.
  • Remove the trigger label (e.g., robodev) if appropriate, so the ticket is not picked up again on the next poll.

MarkComplete

Transitions a ticket to complete with the task result. Called when the agent job finishes successfully.

rpc MarkComplete(MarkCompleteRequest) returns (MarkCompleteResponse);

message MarkCompleteRequest {
  string ticket_id = 1;
  TaskResult result = 2;
}

The TaskResult message includes:

Field Type Description
success bool Whether the task completed successfully
merge_request_url string URL of the created pull request (may be empty)
branch_name string The branch containing the agent's changes
summary string Human-readable summary of what was done
token_usage TokenUsage Input and output token counts
cost_estimate_usd double Estimated cost in US dollars
exit_code int32 0=success, 1=agent failure, 2=guard rail blocked

Implementation guidance:

  • Post a summary comment with a link to the PR before closing the ticket, so humans can easily find the result.
  • The merge_request_url may be empty if the engine did not create a PR (e.g., the task was a linting fix committed directly, or the quality gate blocked PR creation).
  • Include cost and token usage in the comment if your organisation tracks AI spend.

MarkFailed

Transitions a ticket to failed state. Called when the agent job fails and retries are exhausted.

rpc MarkFailed(MarkFailedRequest) returns (MarkFailedResponse);

message MarkFailedRequest {
  string ticket_id = 1;
  string reason = 2;
}

Implementation guidance:

  • Post the failure reason as a comment on the ticket so humans can investigate.
  • Consider adding a label (e.g., robodev-failed) rather than closing the ticket, so it can be retried manually by re-adding the trigger label.
  • Include enough context in the reason for a human to understand what went wrong without consulting controller logs.

AddComment

Posts a progress comment on the ticket. Called during long-running tasks to provide visibility.

rpc AddComment(AddCommentRequest) returns (AddCommentResponse);

message AddCommentRequest {
  string ticket_id = 1;
  string comment = 2;
}

Implementation guidance:

  • Keep comments concise and informative. Avoid flooding the ticket with low-value updates.
  • Consider formatting comments with markdown (most issue trackers support it).
  • This method is fire-and-forget — errors are logged but do not block the controller.

Built-in: GitHub Issues

The GitHub Issues backend (pkg/plugin/ticketing/github/) polls GitHub Issues via the GitHub REST API.

Configuration

config:
  ticketing:
    backend: github
    config:
      owner: "your-org"
      repo: "your-repo"
      labels:
        - "robodev"
      token_secret: "robodev-github-token"

Behaviour

Method GitHub Action
PollReadyTickets GET /repos/{owner}/{repo}/issues?labels=robodev&state=open
MarkInProgress Adds the robodev-in-progress label, removes the robodev label
MarkComplete Posts a comment with the PR link and summary, then closes the issue
MarkFailed Posts a comment with the failure reason, adds the robodev-failed label
AddComment POST /repos/{owner}/{repo}/issues/{number}/comments

Required Permissions

The GitHub personal access token (or GitHub App installation token) needs the following scopes:

Scope Reason
repo Read/write access to repository contents (for agent work)
issues Read/write access to issues (for polling and lifecycle management)
pull_requests Create pull requests (used by the SCM backend, not the ticketing backend directly)

For production deployments, prefer GitHub App installation tokens (1-hour expiry, scoped to specific repositories) over long-lived personal access tokens.

Built-in: Shortcut

The Shortcut backend (pkg/plugin/ticketing/shortcut/) polls Shortcut stories via the Shortcut REST API v3.

Configuration

config:
  ticketing:
    backend: shortcut
    config:
      token_secret: "robodev-shortcut-token"
      workflow_state_name: "Ready for Development"
      in_progress_state_name: "In Development"
      completed_state_name: "Ready for Review"     # optional
      owner_mention_name: "robodev"
      exclude_labels:
        - "robodev-failed"

For workspaces with multiple workflows, use the workflows array instead of the flat state name keys (see the Configuration Reference).

Behaviour

Method Shortcut Action
PollReadyTickets Lists stories in the configured trigger state, optionally filtered by assignee
MarkInProgress Moves the story to in_progress_state_name
MarkComplete Posts a comment with the PR link, summary, cost, and token usage; moves the story to completed_state_name (or the first done-type state if not configured)
MarkFailed Posts a comment with the failure reason; adds the robodev-failed label
AddComment POST /api/v3/stories/{id}/comments

Required Permissions

The Shortcut API token needs member-level access. No special scopes are required beyond what the default member role provides — the token can read and update stories and post comments.


Built-in: Linear

The Linear backend (pkg/plugin/ticketing/linear/) polls Linear issues via the Linear GraphQL API.

Configuration

config:
  ticketing:
    backend: linear
    config:
      token_secret: "robodev-linear-token"
      team_id: "YOUR_TEAM_UUID"
      state_filter: "Todo"
      labels:
        - "robodev"
      exclude_labels:
        - "in-progress"
        - "robodev-failed"

Behaviour

Method Linear Action
PollReadyTickets GraphQL issues query filtered by team, state name, and labels
MarkInProgress Adds the in-progress label to the issue
MarkComplete Posts a comment with the PR link and summary; transitions the issue to the completed state
MarkFailed Adds the robodev-failed label; posts a comment with the failure reason
AddComment commentCreate GraphQL mutation

Required Permissions

The Linear API key needs read and write access to issues and comments in the target team. Create a key under Settings → API → Personal API keys. For service accounts, use a team-scoped API key under Settings → API → OAuth applications.


Writing a Custom Ticketing Backend

See the Writing a Plugin guide for complete examples in Go, Python, and TypeScript. Key design considerations:

Idempotency

All state transition methods (MarkInProgress, MarkComplete, MarkFailed) must be idempotent. The controller may call them more than once due to:

  • Network timeouts followed by retries.
  • The same ticket appearing in consecutive polls before MarkInProgress takes effect.
  • Controller restarts during processing.

Filtering

Return only actionable tickets from PollReadyTickets. The more precise your filtering, the less work the controller does. For example:

  • Exclude tickets that already have an in-progress or robodev-failed label.
  • Only return tickets from repositories in the allowed list (if your backend supports server-side filtering).
  • Limit results to recent tickets to avoid processing stale backlog.

Error Handling

Return appropriate gRPC error codes:

Code When to use
NOT_FOUND Ticket does not exist or has been deleted
UNAVAILABLE The external API is temporarily down
PERMISSION_DENIED Token is invalid or lacks required scopes
INVALID_ARGUMENT Ticket ID is malformed

Timeouts

Respect the context.Context deadline. All RPCs should complete within a reasonable timeout (10–30 seconds). If your ticket source has slow API responses, implement client-side timeouts and retries within the plugin.

Rate Limiting

If your ticket source has API rate limits (e.g., GitHub's 5,000 requests/hour), implement backoff in the plugin rather than relying on the controller. Consider:

  • Caching poll results for a short period.
  • Exponential backoff on rate-limit responses (HTTP 429).
  • Tracking remaining rate limit quota via response headers.

Protobuf Definition

The complete protobuf service is defined in proto/ticketing.proto:

service TicketingBackend {
    rpc Handshake(HandshakeRequest) returns (HandshakeResponse);
    rpc PollReadyTickets(PollReadyTicketsRequest) returns (PollReadyTicketsResponse);
    rpc MarkInProgress(MarkInProgressRequest) returns (MarkInProgressResponse);
    rpc MarkComplete(MarkCompleteRequest) returns (MarkCompleteResponse);
    rpc MarkFailed(MarkFailedRequest) returns (MarkFailedResponse);
    rpc AddComment(AddCommentRequest) returns (AddCommentResponse);
}

See proto/common.proto for the shared Ticket, TaskResult, and HandshakeRequest/HandshakeResponse message definitions.