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

  1. 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.
  2. Validate the actor in your own request-builder constructor so an empty actor never reaches the importer.
  3. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/58bff1fa6656393b. Report an issue: GitHub.