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"] = videoURLView on GitHub (pinned to fc36c76c05)
Solutions
- SELECT id, group_id, attribs FROM bm_contacts WHERE id=<contactID> to see whether the row exists and what attribs contains
- Verify the group_id used at enqueue matches the contact's actual group_id
- Ensure the import pipeline always writes a valid JSON object into attribs, never NULL
- 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
- Never insert bm_video_jobs for a contact until its attribs are confirmed populated
- Use a DB default or NOT NULL JSONB '{}' plus an application check for required keys
- Cascade-delete pending video jobs when a contact is removed
- Keep group_id coupling consistent: derive it from the contact row at enqueue time, not from caller input
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
- failed to get all domains: %w
- fail to check domain: %w
- failed to get all emails: %w
- failed to get all mailboxes: %w
- Failed to get account roles
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/4993c9a0f4995909.
Report an issue: GitHub.