Build a contact-data review list: when a contact is updated, flag a missing email or postal address in your app database. Correcting both details removes the item. The review dashboard shows how many contacts need attention, and the report job saves a summary.
This handler runs inside campai. An integration running on your own server uses webhooks instead.
What you’ll have at the end
A persistent contact-review list that flags missing details, replaces repeated checks, and removes corrected entries.
- Contact update
- Check missing details
- Save or remove review item
- Refresh dashboard
Before you start
- An unpublished app with Database access
- A Record changes group for contacts.updated
- A designated test contact
On this page
In an unpublished test app, enable Settings → Database access, then open Edit → Add group → Record changes. Name the group Record verification and select A contact was updated. Remove the initially selected A posting was created if present.
Use a designated test contact. The example reads the supplied event record and writes only the app's database; it does not change contact details or send messages. It needs no additional organization API permissions.
Replace the group's server index.ts with this complete entry file:
Example
export async function main(action, record) {
if (action !== 'contacts.updated') return
const key = ['contact-review', JSON.stringify(record._id)] as const
const issues: string[] = []
if (!record.communication.email) issues.push('Add an email address')
if (!record.address) issues.push('Add a postal address')
if (issues.length === 0) {
await storage.delete(key)
} else {
await storage.set(key, {
issues,
checkedAt: new Date().toISOString(),
})
}
console.log({ review: issues.length ? 'needs-attention' : 'complete', issues })
}| Code | Why it is there |
|---|---|
action === 'contacts.updated' |
Selects the full event name. The second argument is the supplied contact, not a list of changed fields. |
record.communication.email |
An empty string means no primary email is stored. This is not a newsletter-consent check. |
record.address |
A null address needs attention. Add your own completeness rules if your process requires specific address fields. |
JSON.stringify(record._id) |
Encodes the supplied ID as an opaque string key. The handler does not need to convert it into a REST parameter. |
storage.set / storage.delete |
Replaces that contact's review item or removes it when the checks pass. Repeated input does not append duplicate tasks. |
The stored value contains only the issue list and check time. JSON.stringify preserves the supplied ID representation in the key, including an extended-JSON ObjectId. It is not an ID validator; this handler receives a typed campai record. The original contact remains in campai. App groups share this database, which lets a separate widget read these entries.
Save, choose Simulate, wait for validation and build, then select A contact was updated and enter your test contact's ID. Choose Start simulation. Supplying an ID makes the test reproducible; leaving it blank may select another available contact.
The simulator loads that contact without performing an update. Check these cases using disposable test contacts:
| Contact details | Expected result |
|---|---|
| No email and no address | One stored review item with both issues; log says needs-attention. |
| Same contact simulated again | Still one item, not two. |
| Email added, address still missing | The same item now contains only Add a postal address. |
| Both details present | That contact's review item is removed; log says complete. |
Open the widget example in the same app and refresh it after each case. This verifies the persisted result across separate executions, rather than trusting a log message alone.
Manual simulation tests your handler and database writes. To check automatic delivery, publish a contained test app, change one disposable test contact, and inspect Logs. Do not press Simulate during that delivery check. Refresh the dashboard and verify that the change reached the review list.
The example never writes the contact back, so its own database update does not cause a contact-update loop. If you later update a contact through api, compare the relevant value before writing and grant only the necessary permissions.
Add checks by pushing another actionable issue into issues, such as a missing phone number when your workflow needs one. Keep issue names consistent with the widget and report counters. Never treat having an email address as permission to send marketing.
This list covers contacts whose updates reached the handler. It is not an audit of every contact in the organization. A contact deleted later needs a separate contacts.deleted branch to remove its review entry. The supported event families are contacts, postings, debtors, and creditors, each with created, updated, and deleted variants. Bookings and calendar events have no equivalent trigger here.
Event records are snapshots. Delayed events can replace a newer check with an older snapshot; use a current Get contact read and periodic reconciliation if your workflow requires an authoritative current list. That extension needs app API permissions. Do not use this advisory review queue to decide access, billing, or other irreversible actions.
| Symptom | Check |
|---|---|
| Database request denied | Enable Database access for this app, then rerun the simulation. |
| No item after a successful run | A complete contact is removed from the queue by design. Check the logged issue list. |
| Dashboard stays at zero | Both groups must belong to the same app and use the contact-review prefix. Refresh after the handler finishes. |
| No automatic run | Confirm the published version, configured trigger, and actual event delivery separately from manual simulation. |
Keep the widget's Scope restricted to administrators. Record identifiers and simulation diagnostics need appropriate access even though the dashboard returns only counts.
Check your result
- Missing email and address
- One stored item with both issues.
- Repeated input
- The queue still contains one item.
- Corrected details
- The item disappears after processing and refreshing.