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
Dependencystruct ininternal/types/types.go, storing source/target issue IDs and relationship types. - Type safety is enforced through
DependencyTypeconstants with validation methodsIsValid(),IsWellKnown(), andAffectsReadyWork(). - Transactional writes proceed through
Store.AddDependencyandAddDependencyInTxininternal/storage/issueops/dependencies.go, which validate constraints and update counters atomically. - Cycle detection runs automatically for blocking edge types via
detectCyclesininternal/tracker/engine.go. - Ready-work semantics depend on
AffectsReadyWork(), allowing non-blocking edges likerelatedto exist without impacting issue readiness. - Metadata support enables advanced features like
waits-forgates 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →