- Automatically create and sync fields from Airtable to contact properties in Loops
- Automatically add people from Airtable to lists on Loops without re-subscribing them if they've already unsubscribed
Supported fields:
- Fields (your table in Airtable must have a field called "Email" with the person's email):
Loops - highwaySignedUpAtwill upsert the Airtable value for the row into Loops (if the Airtable field is null, it will not overwrite the Loops value)Loops - Override - highwaySignedUpAtwill copy the Airtable value for the row into Loops, overwriting any existing value in LoopsLoops - Special - setFullName- Uses AI to break a full name (including preferred name) into parts for Loops. This is a good way to get high quality names in Loops.Loops - Special - setFullAddress- Uses AI to break a given full address into individual parts. This does not geocode. It just breaks into parts.Loops List - {Note}- Set value to a comma separated list of mailing list IDs to add the email to. This will not re-subscribe users who have previously unsubscribed from a list.- Ex. Field name:
Loops List - 3 Cool Things. Value:cm96o4ubl02b30i1n6l1jctbb
- Ex. Field name:
First, identify which properties you want to set in Loops. You should always do programSignUpAt at a minimum.
It is better to create timestamped properties than boolean properties.
- Create
highwaySignUpAt -> timestampinstead ofhighwaySignUp -> true/false - Create
hackatimeFirstHeartbeatReceivedAt -> timestampinstead ofhackatimeReceivedHeartbeat -> true/false.
- Create a field in your Airtable called
Loops - {eventName}(must be lowerCamelCase, ex.Loops - highwaySignedUpAt). If you are running a YSWS, the event MUST be prefixed with your YSWS's name (the lowerCamelCase name of your program in the "YSWS Programs" table of the Unified YSWS Projects DB). Examples:Athena Award -> athenaAward,Hackcraft Mod Edition -> hackcraftModEdition,Milkyway -> milkyway.
-
Verify that you table has a column called "Email" (case insenstiive)
-
When you set a value to
Loops - toyboxSignUpAt, it will automatically be picked up and synced to Loops. In most cases, it should happen in under 5 minutes. -
Recommendation: I recommend using
Created timefield types for fields likeLoops - toyboxSignUpAt. If you have a field likeProject Name, I recommend using Formula fields to sync that value to a field calledLoops - toyboxProjectName(instead of writing directly into the field).
Currently supported:
- Select specific Airtable bases to sync
/subscriptions lets anyone verify an email with a one-time code, then subscribe it (creating the Loops contact if it's new), resubscribe after a past unsubscribe, unsubscribe, or choose which public mailing lists they're on. Changes don't call Loops from the web request. Each save is queued as a loops_outbox_envelopes row (provenance.sync_source_type = "self_service", with who asked, from where, and the state they saw) and sent by LoopsDispatchWorker, the same per-email-locked, rate-limited pipeline the Airtable sync uses. After Loops confirms, the dispatcher writes one loops_contact_change_audits row per changed field (is_self_service = true, linked to the envelope and Loops request id) and updates baselines. Failed sends are marked failed with the error and write no audit. List opt-outs are recorded in loops_list_subscriptions so the Airtable sync never re-adds them; an explicit rejoin on this page bypasses that check. The page shows pending changes immediately and a "Recent changes" log (pending, failed and completed changes, from this page or from syncs); admins see the same audit rows, tagged self-service, on /emails/:email.
# required - airtable token with access to whatever bases you want it to poll
AIRTABLE_PERSONAL_ACCESS_TOKEN=
# required - for updating loops contacts
LOOPS_API_KEY=
# required - for splitting addresses and full names into individual parts, and for alt unsubscribe contact merging
OPENAI_API_KEY=
# optional - OpenAI model / reasoning effort for name+address splitting (default: gpt-5.6-luna / low)
LLM_MODEL=
LLM_REASONING_EFFORT=
# optional - OpenAI model / reasoning effort for alt unsubscribe contact merging (default: gpt-5.6-terra / high)
LLM_MERGE_MODEL=
LLM_MERGE_REASONING_EFFORT=
# optional - ID of the mailing list to automatically add all new contacts to, ex. "Announcements"
DEFAULT_LOOPS_LIST_ID=
# optional - URL of a readonly user to the prod database for easier debugging
PROD_READONLY_DATABASE_URL=
# URL for the read-only warehouse database containing the loops.audience table
WAREHOUSE_LOOPS_AUDIENCE_READONLY_URL=
# required - Transactional email ID for sending alt unsubscribe results
LOOPS_ALT_UNSUBSCRIBE_RESULTS_TRANSACTIONAL_ID=
# required - Transactional email ID for sending OTP codes
LOOPS_OTP_TRANSACTIONAL_ID=
# must be set on 1 of the sidekiq workers. this will cause that worker to queue cron jobs
SIDEKIQ_SCHEDULER=1
- Write-back to SyncSource with status so users can see success / error
- Maybe pull from envelopes? Show detailed errors
- For
LlmCache, we probably don't need to store the full request. Hash is probably enough.

