gastownhall/beads · error · publicops.ErrValidation
mixed regular/wisp CreateIssues batch cannot include cross-b
Error message
mixed regular/wisp CreateIssues batch cannot include cross-bucket dependency %s -> %s; create the issues first, then add the in-batch dependency after both issues exist%.0w
What it means
CrossPlaneBatchEdgeError refuses a CreateIssues batch that mixes regular (durable) and wisp (ephemeral) issues while also declaring a dependency whose source and target land in different buckets. Such a cross-plane edge cannot be written by the batch that creates both ends; it must be added after both issues exist. Wraps publicops.ErrValidation.
Source
Thrown at internal/storage/issueops/create_batch.go:193
// counter bookkeeping here.
tables := ChangedTables{}
tables.Merge(CreateIssuesDirtyTables(ctx, issues, created))
result := publicops.CreateBatchResult{Issues: make([]*types.Issue, len(issues))}
for i, issue := range issues {
hydrated, err := HydrateIssueOperationResult(ctx, tx, issue.ID, false)
if err != nil {
return publicops.CreateBatchResult{}, nil, err
}
result.Issues[i] = hydrated
}
return result, tables, nil
}
// CrossPlaneBatchEdgeError is the refusal the store-backed body raises from
// filterCreateIssuesMixedBucketDependencies, spelled here so the unit-of-work
// body can raise the identical one before it starts writing.
func CrossPlaneBatchEdgeError(sourceID, targetID string) error {
return fmt.Errorf("mixed regular/wisp CreateIssues batch cannot include cross-bucket dependency %s -> %s; create the issues first, then add the in-batch dependency after both issues exist%.0w",
sourceID, targetID, publicops.ErrValidation)
}
// ValidateCreateBatchPlanes refuses a batch whose items would write an edge
// BETWEEN the durable and ephemeral planes, which BatchCreator's contract says
// "cannot be written by the batch that creates both of its ends".
//
// THE STORE BODY GETS THIS FOR FREE and the unit-of-work body does not. The
// store body assigns every id first and then hands the whole slice to
// CreateIssuesInTxWithResult, which sees the batch as a set and refuses. The
// unit-of-work body creates item by item, so by the time an edge is written
// its target is an ordinary existing row and the domain layer writes the
// cross-plane edge happily — the identical request was refused whole by two
// backends and landed in full on the third.
//
// Only an EXPLICIT id can be an in-batch target: a caller cannot name an id the
// batch has not minted yet, so the ids known here are the only ones an edge can
// reach inside the batch. infraTypes promotes the same types both bodiesView on GitHub (pinned to 71377f2769)
Solutions
- Split the batch: create both issues first (possibly in separate batches per plane), then issue a second dependency-add call for the cross-bucket edge
- Convert one endpoint so both ends of the dependency live in the same bucket (both regular or both wisp)
- Pre-screen items: for each dependency, check plane(item.Issue) == plane(dep.Target) before submitting the batch
Example fix
// before
batch.Dependencies = []publicops.CreateDependency{{IssueID: regularID, DependsOnID: wispID}}
// after
ExecuteCreateBatch(ctx, batchWithoutCrossEdge)
AddDependency(ctx, regularID, wispID) // after both exist Defensive patterns
Strategy: validation
Validate before calling
func isWisp(i *types.Issue) bool { /* wisp predicate */ }
for _, item := range req.Items {
plane := map[string]bool{}
for _, d := range item.Dependencies {
if plane[d.IssueID] != plane[d.DependsOnID] {
return fmt.Errorf("cross-plane edge %s -> %s", d.IssueID, d.DependsOnID)
}
}
} Try / catch
if err := ExecuteCreateBatch(ctx, req); errors.Is(err, publicops.ErrValidation) && strings.Contains(err.Error(), "cross-bucket dependency") {
// split batch and add edges afterwards
} Prevention
- Never declare dependencies across regular/wisp items in one batch
- Group imports per plane before batching
- Add cross-plane edges in a follow-up dependency call after both creates succeed
When it happens
Trigger: A single CreateIssues batch containing e.g. a regular issue A depending on wisp issue B (or vice versa) via item Dependencies, where the edge crosses the durable/ephemeral plane boundary.
Common situations: Bulk-importing mixed-priority work where a long-lived blocker references a transient wisp; tools generating one batch from a graph that does not distinguish wisps from regular issues.
Related errors
- db: DependencySQLRepository.Insert: dep must not be nil
- db: DependencySQLRepository.Insert: IssueID must not be empt
- db: DependencySQLRepository.Insert: DependsOnID must not be
- db: DependencySQLRepository.ValidateBlockingHierarchy: dep m
- %w: apply batch item %d: %s cannot depend on itself
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/155a0557fcf2bf95.
Report an issue: GitHub.