Billionmail/BillionMail · error

contact %d has no attribs

Error message

contact %d has no attribs

What it means

loadContactAttribs raises this when the query executed successfully but the attribs value is nil or empty — meaning no matching bm_contacts row (wrong id or group_id) or a row whose attribs column is NULL/empty. The pipeline cannot personalize the video without attribs, so the job fails at the load_contact phase. This is a data-existence error, distinct from a query error (466) or a JSON parse error (468).

Source

Thrown at core/internal/service/video_gen/orchestrator.go:332

}

// markFailed sets job status to failed with error message and logs it.
func markFailed(ctx context.Context, jobID int, phase string, err error) {
	g.Log().Errorf(ctx, "video job %d failed at %s: %v", jobID, phase, err)
	updateJobStatus(ctx, jobID, JobFailed, phase, err.Error())
}

// loadContactAttribs fetches attribs for a contact from DB.
func loadContactAttribs(ctx context.Context, contactID, groupID int) (map[string]string, error) {
	val, err := g.DB().Model("bm_contacts").Ctx(ctx).
		Where("id", contactID).
		Where("group_id", groupID).
		Value("attribs")
	if err != nil {
		return nil, fmt.Errorf("load contact attribs: %w", err)
	}
	if val.IsNil() || val.IsEmpty() {
		return nil, fmt.Errorf("contact %d has no attribs", contactID)
	}

	result := make(map[string]string)
	if err := val.Scan(&result); err != nil {
		return nil, fmt.Errorf("parse contact attribs: %w", err)
	}
	return result, nil
}

// updateContactVideoAttribs merges video URLs into Contact.Attribs.
func updateContactVideoAttribs(ctx context.Context, contactID, groupID int, videoURL, thumbURL, landingURL string) error {
	// Load existing attribs
	attribs, err := loadContactAttribs(ctx, contactID, groupID)
	if err != nil {
		return err
	}

	attribs["video_url"] = videoURL

View on GitHub (pinned to fc36c76c05)

Solutions

  1. SELECT id, group_id, attribs FROM bm_contacts WHERE id=<contactID> to see whether the row exists and what attribs contains
  2. Verify the group_id used at enqueue matches the contact's actual group_id
  3. Ensure the import pipeline always writes a valid JSON object into attribs, never NULL
  4. If the contact was deleted, purge or skip its pending bm_video_jobs rows instead of letting them fail each poll cycle

Example fix

// before
result, err := EnqueueVideoJob(ctx, contactID, email, groupID)
// after
exists, _ := g.DB().Model("bm_contacts").Ctx(ctx).Where("id", contactID).Where("group_id", groupID).Count()
if exists == 0 {
    return fmt.Errorf("contact %d not found in group %d", contactID, groupID)
}
result, err := EnqueueVideoJob(ctx, contactID, email, groupID)
Defensive patterns

Strategy: validation

Validate before calling

count, err := g.DB().Model("bm_contacts").Ctx(ctx).
    Where("id", contactID).Where("group_id", groupID).
    Where("attribs IS NOT NULL AND attribs <> '' AND attribs <> '{}'").Count()
if err != nil || count == 0 {
    return fmt.Errorf("contact %d not eligible (missing/empty attribs in group %d)", contactID, groupID)
}

Try / catch

_, err := loadContactAttribs(ctx, contactID, groupID)
if err != nil && strings.Contains(err.Error(), "has no attribs") {
    // permanent data problem: fail the job without retries
    cancelJob(jobID, "contact has no attribs")
}

Prevention

When it happens

Trigger: Query returns no row for (contactID, groupID); the row exists but attribs is NULL; attribs is an empty string or empty JSON object that GoFrame's Value reports as empty.

Common situations: Contact deleted between enqueue and processing; job enqueued with a mismatched group_id (contacts are scoped per group); contact imported without any attribute mapping so attribs stayed NULL; dev/staging DB pointed at a dataset lacking the contact.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/4993c9a0f4995909. Report an issue: GitHub.