How Dependencies Are Represented and Managed in Beads: The Complete DAG Edge Implementation

Dependencies in Beads are modeled as first-class directed edges in a DAG (directed acyclic graph), implemented via the Dependency struct in internal/types/types.go and managed through transactional storage operations that enforce cycle detection and ready-work semantics.

The gastownhall/beads issue tracker treats inter-issue relationships as core graph primitives rather than auxiliary metadata. Every dependency is a typed edge stored in SQL, enabling automatic cycle prevention, dependency-aware ready queues, and rich relationship semantics ranging from simple blocking to conditional gating.

The Dependency Data Model in internal/types/types.go

At the heart of Beads dependency management lies the Dependency struct defined at lines 15-22 of internal/types/types.go.

Core Struct Fields

The struct captures the minimal data required to define a graph edge between two issues:

type Dependency struct {
    IssueID     string         `json:"issue_id"`      // source issue (the blocker)
    DependsOnID string         `json:"depends_on_id"` // target issue (the blocked)
    Type        DependencyType `json:"type"`          // edge semantics
    CreatedAt   time.Time      `json:"created_at"`
    CreatedBy   string         `json:"created_by,omitempty"`
    Metadata    string         `json:"metadata,omitempty"`
    ThreadID    string         `json:"thread_id,omitempty"`
}

This design supports both blocking workflows and conversational threading through the ThreadID field, while the Metadata column stores JSON for extended gate logic.

Dependency Type Constants and Validation

Relationship semantics are defined through the DependencyType string type. The source code at lines 71-84 of internal/types/types.go declares well-known constants:

const (
    DepBlocks            DependencyType = "blocks"
    DepParentChild       DependencyType = "parent-child"
    DepConditionalBlocks DependencyType = "conditional-blocks"
    DepWaitsFor          DependencyType = "waits-for"
    DepRelated           DependencyType = "related"
    DepDiscoveredFrom    DependencyType = "discovered-from"
    DepRepliesTo         DependencyType = "replies-to"
    DepDuplicates        DependencyType = "duplicates"
)

Each type provides three validation methods defined at lines 108-122: IsValid() ensures non-empty strings under 50 bytes, IsWellKnown() identifies built-in constants, and AffectsReadyWork() returns true only for the four blocking types (blocks, parent-child, conditional-blocks, waits-for).

Storage Layer API and Transactional Safety

All dependency mutations flow through the storage interface defined in internal/storage/storage.go.

The Store Interface

The interface at lines 44-52 declares the primary entry points for dependency creation:

type Store interface {
    AddDependency(ctx context.Context, dep *types.Dependency, actor string) error
    AddDependencyWithOptions(ctx context.Context, dep *types.Dependency,
        actor string, opts DependencyAddOptions) error
}

Transactional Implementation in issueops/dependencies.go

Concrete store implementations delegate to AddDependencyInTx in internal/storage/issueops/dependencies.go (lines 39-61). This helper executes four operations atomically within a SQL transaction: validating the edge (preventing self-dependencies), inserting into the dependencies table, updating dependency counters via queries defined at lines 30-44 of internal/storage/issueops/dependency_queries.go, and emitting EventDependencyAdded for audit trails.

The function accepts behavioral options defined at lines 13-19:

func AddDependencyInTx(ctx context.Context, tx *sql.Tx,
    dep *types.Dependency, actor string, opts AddDependencyOpts) error

Cycle Detection and High-Level Engine Logic

The tracker engine in internal/tracker/engine.go orchestrates writes with business logic for graph integrity.

When addDependency is invoked at lines 1005-1013, it persists the edge through the store, then conditionally runs detectCycles only for blocking types like DepBlocks. If a cycle is detected, the transaction rolls back, preventing DAG corruption. Finally, the engine invalidates the ready-work cache to ensure queue accuracy.

This cycle detection uses GetAllDependencyRecordsInTx to traverse the graph and verify acyclicity before commit.

Ready-Work Calculation and Blocking Semantics

Not all edges block execution. The AffectsReadyWork() method determines whether a dependency type impedes issue readiness. During ready-work calculation, BuildReadyExplanation defined at lines 267-285 of internal/types/types.go consults this flag to decide if an issue is blocked.

Blocking edges (blocks, parent-child, conditional-blocks, waits-for) prevent the dependent issue from entering the ready queue until resolved. Non-blocking edges (related, discovered-from, replies-to) maintain graph connectivity for knowledge management without affecting workflow state.

Working with Dependencies: Code Examples

Adding a Blocking Dependency Programmatically

To create a standard blocker relationship:

ctx := context.Background()
dep := &types.Dependency{
    IssueID:     "bd-99",
    DependsOnID: "bd-42",
    Type:        types.DepBlocks,
    CreatedAt:   time.Now(),
    CreatedBy:   "alice",
}
if err := store.AddDependency(ctx, dep, "alice"); err != nil {
    log.Fatalf("failed to add dependency: %v", err)
}

Creating Waits-For Gates with Metadata

The waits-for type supports sophisticated gating through JSON metadata:

meta, _ := json.Marshal(types.WaitsForMeta{Gate: types.WaitsForAllChildren})
dep := &types.Dependency{
    IssueID:     "gate-1",
    DependsOnID: "task-7",
    Type:        types.DepWaitsFor,
    Metadata:    string(meta),
    CreatedAt:   time.Now(),
}
store.AddDependency(ctx, dep, "system")

CLI Invocation

The bd command-line tool documented in docs/CLI_REFERENCE.md (lines 788-795) exposes the same storage flow:

bd dep add bd-99 blocks bd-42

This parses arguments into a Dependency struct and invokes the tracker engine.

Summary

  • Dependencies in Beads are modeled as directed edges in a DAG using the Dependency struct in internal/types/types.go, storing source/target issue IDs and relationship types.
  • Type safety is enforced through DependencyType constants with validation methods IsValid(), IsWellKnown(), and AffectsReadyWork().
  • Transactional writes proceed through Store.AddDependency and AddDependencyInTx in internal/storage/issueops/dependencies.go, which validate constraints and update counters atomically.
  • Cycle detection runs automatically for blocking edge types via detectCycles in internal/tracker/engine.go.
  • Ready-work semantics depend on AffectsReadyWork(), allowing non-blocking edges like related to exist without impacting issue readiness.
  • Metadata support enables advanced features like waits-for gates with JSON configuration.

Frequently Asked Questions

What data structure does Beads use to store dependencies?

Beads stores dependencies as SQL table rows represented by the Dependency struct in internal/types/types.go (lines 15-22). Each row contains IssueID (source), DependsOnID (target), Type (relationship semantics), timestamps, and optional metadata fields.

How does Beads prevent circular dependencies?

The tracker engine in internal/tracker/engine.go invokes detectCycles immediately after inserting blocking edges. It traverses the dependency graph using GetAllDependencyRecordsInTx and returns an error if the new edge would create a cycle, causing the SQL transaction to rollback and preserving DAG integrity.

What is the difference between blocks and waits-for dependency types?

The blocks type creates a standard blocking relationship where the source issue must be resolved before the target is considered ready. The waits-for type implements gating logic and can store JSON metadata (parsed via ParseWaitsForGateMetadata) specifying conditions like all-children or any-children completion modes for complex workflow control.

Can I create custom dependency types in Beads?

Yes. The DependencyType accepts any non-empty string under 50 bytes. While only built-in constants return true for IsWellKnown(), custom types are valid and treated as non-blocking edges that do not affect ready-work calculations unless your application logic specifically recognizes them.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →