gastownhall/beads · error
import batch: actor must not be empty
Error message
import batch: actor must not be empty
What it means
ImportBatch validates up front that ImportBatchRequest.Actor is non-empty and rejects the whole batch before any transaction is started. The actor identifies who performed the import and is threaded into the batch-upsert engine; without it the write would be unattributable, so the library refuses to run.
Source
Thrown at internal/storage/uow/importer.go:51
provider UnitOfWorkProvider
}
var _ publicops.Importer = (*importer)(nil)
// ImportBatch writes the whole batch in ONE unit of work and commits it as
// ONE history entry: the issue rows through the SAME batch-upsert engine the
// classic stores run (internal/storage/issueops.CreateIssuesInTxWithResult —
// conditional row upsert, idempotent label/comment/dependency merge, child
// counters, blocked recompute), the memory records, and the optional
// issue_prefix reconciliation. A request-level failure rolls all of it back.
//
// The engine's callbacks land in the result instead of being exposed on the
// request: RunTxResult retries the whole attempt on a serialization failure,
// and result state declared inside the attempt cannot leak between retries
// the way a caller's callback accumulator would.
func (o *importer) ImportBatch(ctx context.Context, request publicops.ImportBatchRequest) (publicops.ImportBatchResult, error) {
if request.Actor == "" {
return publicops.ImportBatchResult{}, fmt.Errorf("import batch: actor must not be empty")
}
return RunTxResult(ctx, o.provider, func(ctx context.Context, uw UnitOfWork) (publicops.ImportBatchResult, string, error) {
var result publicops.ImportBatchResult
if len(request.Issues) > 0 {
runner, err := importStatementRunner(uw)
if err != nil {
return publicops.ImportBatchResult{}, "", err
}
staleRejected := make(map[string]struct{})
skippedSeen := make(map[string]struct{})
opts := storage.BatchCreateOptions{
SkipPrefixValidation: request.SkipPrefixValidation,
RejectStaleUpserts: !request.AllowStale,
SkipDependencyValidationErrors: true,
OnSkippedDependency: func(issueID, dependsOnID, reason string) {
key := issueID + "\x00" + dependsOnID + "\x00" + reason
if _, ok := skippedSeen[key]; ok {View on GitHub (pinned to 71377f2769)
Solutions
- Set request.Actor to the acting identity (e.g. os.Getenv("USER")/git config user, or the value the CLI already computes) before calling ImportBatch.
- Validate the actor in your own request-builder constructor so an empty actor never reaches the importer.
- If migrating from the classic path, pass the same actor string the batch-upsert engine previously received.
Example fix
// before
req := publicops.ImportBatchRequest{Issues: issues, Source: "bd import"}
result, err := imp.ImportBatch(ctx, req) // "actor must not be empty"
// after
req := publicops.ImportBatchRequest{Actor: currentUser(), Issues: issues, Source: "bd import"}
result, err := imp.ImportBatch(ctx, req) Defensive patterns
Strategy: validation
Validate before calling
func validateImportRequest(req publicops.ImportBatchRequest) error {
if req.Actor == "" {
return errors.New("import batch: actor must not be empty")
}
return nil
}
// call before ImportBatch
if err := validateImportRequest(req); err != nil {
return fmt.Errorf("build import request: %w", err)
} Type guard
func hasActor(req publicops.ImportBatchRequest) bool {
return strings.TrimSpace(req.Actor) != ""
} Try / catch
result, err := imp.ImportBatch(ctx, req)
if err != nil {
if strings.Contains(err.Error(), "actor must not be empty") {
return fmt.Errorf("caller bug: set ImportBatchRequest.Actor (e.g. current user) before importing: %w", err)
}
return err
} Prevention
- Default Actor at request-construction time from the same identity the CLI uses.
- Make request builders require the actor as a constructor argument.
- Add a unit test asserting ImportBatch rejects empty actors, so builders stay honest.
- Never serialize/deserialize requests dropping the Actor field.
When it happens
Trigger: Calling ImportBatch (publicops.Importer) with a zero-value ImportBatchRequest, or a request built programmatically/tests where the Actor field was never set — including when Issues and Memories are both empty (validation happens before content checks).
Common situations: Scripted `bd import` pipelines that construct ImportBatchRequest themselves and forget to propagate the current user; refactors that renamed the actor field and dropped the assignment; tests that build requests via composite literals without Actor.
Related errors
- proxy.ForceStopUnverified: at most one options value is allo
- import failed: %w
- proxy.ForceStopUnverified: timeout must be positive, got %s
- issue must not be nil
- add dep: dep must not be nil
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/58bff1fa6656393b.
Report an issue: GitHub.