Ingest Brabys subscriber data from Informix file dumps into a PostgreSQL staging database as raw tables — exact mirrors of the Informix schema. General orchestrates the queue — pulling records from the DB and allocating them to Claire in batches. Claire calls each business to confirm or update data (phone, email, address, business name). Scrub validates Claire’s updates before write-back. Verified changes are written back to Carl in the same file-dump format for re-import into Informix production.
🟢 E2E PIPELINE PROVEN. All 13 Informix tables loaded. 21,872 subscribers in PostgreSQL staging. Records 301, 351, 352 reached APPROVED status via real outbound calls (Twilio → Claire → webhook → Scrub → APPROVED). Concurrent dispatch live (up to 2 simultaneous calls). Webhook-first chaining operational. Schema-aware Scrub loads campaign schema and uses approval thresholds. Campaign templates and creation wizard deployed. 3CX SIP still blocked — using Twilio workaround.
Snitch (QA Agent) — added March 2026: ElevenLabs agent agent_1801kkftqqzxe8bvmwm244ash495 with 6 test scenarios. Calls Claire as a simulated business to verify conversation quality. Shown on the brabys-agents.html dashboard page alongside Claire, Scrub, and General.
This is NOT a live database connection. All data flows via file dumps — inbound and outbound. The staging DB holds raw Informix tables only — no flattening or transformation. All phone reconstruction and address resolution happens in the read layer (PostgreSQL views) at query time.
Informix 11 running on AIX 5.3. Carl provides file dumps as a ZIP archive containing 13 pipe-delimited text files — one file per table, one row per record.
Each dump contains the following 13 tables:
| Table | Purpose |
|---|---|
| cosdienr | Core subscriber record — address codes, flags, primary key |
| cosbusnm | Business name(s) per subscriber |
| cosphone | Phone numbers (multiple per subscriber) |
| cosinfod | Email addresses and website URLs |
| cospobox | PO Box address |
| cosaltad | Free-text alternate address (non-standard) |
| cosstrdc | Street code lookup |
| cossubrb | Suburb code lookup |
| costown | Town/city lookup (includes dialing code) |
| cosblddc | Building code lookup |
| cosregion | Region lookup |
| coscountry | Country lookup |
| cosplot | Plot type lookup |
Pretoria / 012 dialing area — fully loaded into PostgreSQL staging:
de_alpha_key|de_town_code|de_suburb_code|de_street_code|de_street_no|...
1234567|012|4501|88210|42|...
|) separator.sql filesThe cosdienr table is the core subscriber record. Every other table joins back to it via de_alpha_key. Understanding these fields is essential for import, filtering, and write-back.
| Field | Type | Description |
|---|---|---|
| de_alpha_key | INTEGER | Primary key, immutable — links all tables |
| de_town_code | FK | Foreign key → costown (tw_town_code) |
| de_suburb_code | FK | Foreign key → cossubrb (sb_suburb_code) |
| de_street_code | FK | Foreign key → cosstrdc (st_street_code) |
| de_build_code | FK | Foreign key → cosblddc (building name) |
| de_street_no | INTEGER | Street number |
| de_alt_str_no | CHAR(1) | Letter suffix for street number (e.g. a in 12a Caversham Road) |
| de_build_no | INTEGER | Building / unit number |
| de_alt_bld_no | CHAR(6) | Letter/suffix for building number (e.g. b in 27b Sanlam Building) |
| de_floor | CHAR(2) | Floor (for buildings) |
| de_corner_of | VARCHAR | Cross street text (e.g. Underwood Road &). Code prepends Cnr. — main street resolved via cosstrdc |
| de_amend_date | DATE | Last amended date (dd/mm/yyyy) |
| Field | Type | Description |
|---|---|---|
| de_print_code | CHAR | Print filter flag — NULL / " " = printable |
| de_z_entry | CHAR | Exclude flag — NULL / " " = include |
| de_specific | CHAR | Specific-type entry flag |
| de_unlisted | CHAR | Unlisted flag |
| de_sch_do_not_call | CHAR | Do Not Call flag — MUST be checked before any outbound call |
Records are excluded from the working set if: de_print_code is not null/space, de_z_entry is set, de_specific is set, or bn_bus_name starts with ,, or contains DELETE. These mirror the filters Carl uses in sql.sql.
Phone numbers are stored as INTEGER in cosphone (ph_phone), which means leading zeros are stripped on write. Reconstruction logic is derived from address.4gl.
Two fields determine the phone type: ph_phone_ind and ph_fax_ind.
| Code | Type | Reconstruction |
|---|---|---|
| "Y" | Phone (landline) | Prepend tw_dialing_code from costown |
| "C" | Cell / Mobile | South Africa: prepend "0"; otherwise as-is |
| "V" | VOIP | South Africa: prepend "0"; otherwise as-is |
| "S" | Sharecall | South Africa: prepend "0"; otherwise as-is |
| "T" | Toll-free | South Africa: prepend "0"; otherwise as-is |
| Code | Type | Reconstruction |
|---|---|---|
| "Y" | Fax | Prepend tw_dialing_code |
| "C" | CellFax | South Africa: prepend "0" |
| "V" | VOIPFax | South Africa: prepend "0" |
| "X" | ShareCallFax | South Africa: prepend "0" |
cosphone.ph_alpha_key = cosdienr.de_alpha_key
→ cosdienr.de_town_code = costown.tw_town_code
→ use costown.tw_dialing_code
Landline: ph_phone = 123456, tw_dialing_code = "012" → reconstructed = "012123456"
Mobile: ph_phone = 821234567, ph_phone_ind = "C", country = SOUTH AFRICA → "0821234567"
Only attempt calls where ph_phone_ind = "Y" or "C" AND de_sch_do_not_call is NULL or " ". This is a hard rule — no exceptions.
Street addresses are assembled from code lookups as defined in address.4gl (get_street_address function). Alternate free-text addresses take priority when present.
Addresses are assembled as five lines, with empty lines omitted:
| Line | Components | Source |
|---|---|---|
| 1 — Building | bd_build_name + bd_abb_desig + de_build_no + de_alt_bld_no + de_floor + de_plot_flag | cosblddc + cosdienr |
| 2 — Street | de_street_no + de_alt_str_no + st_street_name + st_abb_desig; if de_corner_of present, prepend “Cnr.” | cosdienr + cosstrdc |
| 3 — Suburb | sb_suburb_name | cossubrb |
| 4 — Town | tw_town_name | costown |
| 5 — Postal Code | sb_str_post_code (or country if none) | cossubrb |
cosaltad.alt_address replaces Lines 1 + 2 only. Lines 3–5 (suburb, town, postal code) always come from codes.
The building table (cosblddc) is joined with a conditional on bd_street_code: if both de_street_code and bd_street_code are zero/null, they match; otherwise bd_street_code must equal de_street_code. This handles buildings that span multiple streets or have no street association.
Address changes must be written back as code updates to cosdienr, NOT free text. If the new address cannot be resolved to valid Informix codes (cosstrdc / cossubrb / costown), it MUST go to cosaltad instead.
The staging database keeps raw tables only — exact mirrors of the Informix schema. No flattening or reconstruction is stored in the database. All code resolution and phone reconstruction happens in the read layer (Section 7) at query time.
Exact column-for-column mirrors of the Informix originals. One raw_ table per source file:
| Table | Purpose | Join Key |
|---|---|---|
| raw_cosdienr | Core subscriber record — address codes, flags, PK | de_alpha_key (PK) |
| raw_cosbusnm | Business name(s) per subscriber | bn_alpha_key → de_alpha_key |
| raw_cosphone | Phone numbers (multiple per subscriber) | ph_alpha_key → de_alpha_key |
| raw_cosinfod | Email addresses and website URLs | inf_alpha_key → de_alpha_key |
| raw_cospobox | PO Box address | po_alpha_key → de_alpha_key |
| raw_cosaltad | Free-text alternate address (non-standard) | alt_alpha_key → de_alpha_key |
Reference tables used by the read layer to resolve codes into human-readable values. Never modified by agents:
| Table | Purpose | Primary Key |
|---|---|---|
| raw_costown | Town / city lookup (includes dialing code) | tw_town_code |
| raw_cossubrb | Suburb code lookup | sb_suburb_code |
| raw_cosstrdc | Street code lookup | st_street_code |
| raw_cosblddc | Building code lookup | bd_build_code |
| raw_cosregion | Region lookup | rg_region_code |
| raw_coscountry | Country lookup | cy_country |
| raw_cosplot | Plot type lookup | pl_plot_code |
Tracks each ZIP import for audit and rollback.
CREATE TABLE import_batches (
id SERIAL PRIMARY KEY,
filename TEXT,
imported_at TIMESTAMPTZ,
record_count INTEGER,
notes TEXT
);
AI verification output per subscriber — stores what changed, agent confidence, and current record status.
CREATE TABLE verification_results (
alpha_key INTEGER PRIMARY KEY,
verified_at TIMESTAMPTZ,
verified_business_name TEXT,
verified_phone TEXT,
verified_email TEXT,
verified_website TEXT,
verified_address TEXT,
confidence_score NUMERIC,
changes_made JSONB, -- what changed
call_transcript TEXT,
agent_notes TEXT,
record_status TEXT DEFAULT 'pending'
-- NEW | QUEUED | CALLING | QA_PENDING | APPROVED | REJECTED | CONFLICT | HUMAN_REVIEW | FAILED | DNC | MERGED | SYNCED
);
The delta layer between what Claire captures on a call and what gets written back to Informix. One row per field changed, per call — not one row per subscriber. Scrub writes to this table after parsing the call transcript. The write layer reads APPROVED rows from this table when generating output files.
CREATE TABLE field_updates (
id SERIAL PRIMARY KEY,
record_id INTEGER REFERENCES records(id),
call_log_id INTEGER REFERENCES call_log(id),
field_name VARCHAR(100), -- e.g. 'phone', 'email', 'business_name'
old_value TEXT,
new_value TEXT,
confidence FLOAT, -- Scrub confidence score 0.0–1.0
qa_status VARCHAR(50), -- APPROVED | REJECTED | HUMAN_REVIEW
qa_agent_id VARCHAR(100), -- 'scrub' or human reviewer ID
reviewed_at TIMESTAMPTZ
);
field_updates is the authoritative source for write-back. The write layer ONLY processes rows where qa_status = 'APPROVED'. verification_results stores the call-level summary; field_updates stores the field-level delta.
Handshake table written back to Carl — tells Informix which records are ready to load.
CREATE TABLE tracking (
alpha_key INTEGER,
verified_at TIMESTAMPTZ
);
Campaign configuration table — defines each calling campaign with its associated agent and schema type. Added columns agent_id and schema_type in March 2026.
CREATE TABLE campaigns (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT,
agent_id TEXT, -- ElevenLabs agent ID for this campaign
schema_type TEXT DEFAULT 'verification', -- maps to config/schemas/*.json
status TEXT DEFAULT 'active',
created_at TIMESTAMPTZ,
config JSONB -- campaign-specific settings
);
Campaign schema definitions — JSON files that define expected fields, extraction prompts, and approval thresholds per campaign type. Scrub loads the relevant schema at processing time.
config/schemas/verification.json — standard directory verification fieldsconfig/schemas/debt-collection.json — placeholder for future debt collection campaign typeCampaign templates — pre-configured campaign definitions used by the Campaign Creation Wizard. Each template pre-fills campaign settings from a JSON file.
config/templates/directory-verification.jsonconfig/templates/debt-collection.jsonconfig/templates/renewal-outreach.jsonRaw tables are the single source of truth. The staging DB never stores derived or flattened data. This eliminates sync bugs between raw and flattened representations and makes write-back trivial — output the raw tables directly, applying only the agent's changes.
Agents never query raw tables directly. All reads go through PostgreSQL views or a Node.js API layer. The read layer handles phone reconstruction, address code resolution, business name selection, email/web extraction, and DNC flagging — all computed at query time from raw data.
ph_phone_ind + tw_dialing_codebn_line_no = 1 from raw_cosbusnmraw_cosinfodde_sch_do_not_call on raw_cosdienrverification_resultsPrimary PostgreSQL view — the single interface agents use to read subscriber data:
CREATE OR REPLACE VIEW subscriber_view AS
SELECT
d.de_alpha_key AS alpha_key,
TRIM(b.bn_bus_name) AS business_name,
-- Phone reconstruction (primary landline: ph_phone_ind = 'Y')
MAX(CASE
WHEN TRIM(p.ph_phone_ind) = 'Y' AND p.ph_phone IS NOT NULL AND p.ph_phone != 0
THEN TRIM(tw.tw_dialing_code) || p.ph_phone::text
ELSE NULL
END) AS phone_landline,
-- Mobile (ph_phone_ind = 'C')
MAX(CASE
WHEN TRIM(p.ph_phone_ind) = 'C' AND p.ph_phone IS NOT NULL AND p.ph_phone != 0
THEN '0' || p.ph_phone::text
ELSE NULL
END) AS phone_mobile,
-- Email & Website
MAX(CASE WHEN TRIM(i.fod_info_abbrev) = 'EMAIL' THEN TRIM(i.fod_text) END) AS email,
MAX(CASE WHEN TRIM(i.fod_info_abbrev) = 'WEB' THEN TRIM(i.fod_text) END) AS website,
-- Raw address components
TRIM(bld.bd_build_name) AS bd_build_name,
TRIM(bld.bd_abb_desig) AS bd_abb_desig,
d.de_build_no, TRIM(d.de_alt_bld_no) AS de_alt_bld_no,
TRIM(d.de_floor) AS de_floor, TRIM(d.de_plot_flag) AS de_plot_flag,
d.de_street_no, TRIM(d.de_alt_str_no) AS de_alt_str_no,
TRIM(s.st_street_name) AS st_street_name, TRIM(s.st_abb_desig) AS st_abb_desig,
TRIM(d.de_corner_of) AS de_corner_of,
TRIM(alt.alt_address) AS alt_address,
sub.sb_str_post_code AS postal_code,
-- Address: 5-line assembly (alt_address replaces lines 1+2 only)
CASE
WHEN TRIM(alt.alt_address) IS NOT NULL AND TRIM(alt.alt_address) != ''
THEN CONCAT_WS(E'\n',
NULLIF(TRIM(alt.alt_address), ''),
NULLIF(TRIM(sub.sb_suburb_name), ''),
NULLIF(TRIM(tw.tw_town_name), ''),
NULLIF(TRIM(sub.sb_str_post_code::text), ''))
ELSE CONCAT_WS(E'\n',
-- Line 1: Building
NULLIF(TRIM(CONCAT_WS(' ',
NULLIF(TRIM(bld.bd_build_name),''), NULLIF(TRIM(bld.bd_abb_desig),''),
NULLIF(d.de_build_no::text,'0'), NULLIF(TRIM(d.de_alt_bld_no),''),
NULLIF(TRIM(d.de_floor),''), NULLIF(TRIM(d.de_plot_flag),''))),''),
-- Line 2: Street (corner_of adds Cnr. prefix)
NULLIF(TRIM(CASE
WHEN TRIM(d.de_corner_of) IS NOT NULL AND TRIM(d.de_corner_of) != ''
THEN CONCAT_WS(' ', 'Cnr.', TRIM(d.de_corner_of),
NULLIF(d.de_street_no::text,'0')||COALESCE(NULLIF(TRIM(d.de_alt_str_no),''),''),
NULLIF(TRIM(s.st_street_name),''), NULLIF(TRIM(s.st_abb_desig),''))
ELSE CONCAT_WS(' ',
NULLIF(d.de_street_no::text,'0')||COALESCE(NULLIF(TRIM(d.de_alt_str_no),''),''),
NULLIF(TRIM(s.st_street_name),''), NULLIF(TRIM(s.st_abb_desig),''))
END),''),
-- Lines 3-5: Suburb, Town, Postal Code
NULLIF(TRIM(sub.sb_suburb_name),''),
NULLIF(TRIM(tw.tw_town_name),''),
NULLIF(TRIM(sub.sb_str_post_code::text),''))
END AS address,
TRIM(sub.sb_suburb_name) AS suburb,
TRIM(tw.tw_town_name) AS town,
TRIM(tw.tw_dialing_code) AS dialing_code,
CASE WHEN TRIM(d.de_sch_do_not_call) = 'Y' THEN true ELSE false END AS do_not_call,
d.de_amend_date AS last_amended
FROM raw_cosdienr d
LEFT JOIN raw_cosbusnm b ON b.bn_alpha_key = d.de_alpha_key AND b.bn_line_no = 1
LEFT JOIN raw_cosphone p ON p.ph_alpha_key = d.de_alpha_key
LEFT JOIN raw_cosinfod i ON i.fod_alpha_key = d.de_alpha_key
LEFT JOIN raw_costown tw ON tw.tw_town_code = d.de_town_code
LEFT JOIN raw_cossubrb sub ON sub.sb_suburb_code = d.de_suburb_code
AND sub.sb_town_code = d.de_town_code
LEFT JOIN raw_cosstrdc s ON s.st_street_code = d.de_street_code
AND s.st_town_code = d.de_town_code
AND s.st_suburb_code = d.de_suburb_code
-- Building: conditional join (street_code must match OR both be zero/null)
LEFT JOIN raw_cosblddc bld ON bld.bd_town_code = d.de_town_code
AND bld.bd_suburb_code = d.de_suburb_code
AND bld.bd_build_code = d.de_build_code
AND ((COALESCE(d.de_street_code,0)=0
AND COALESCE(bld.bd_street_code,0)=0)
OR (COALESCE(d.de_street_code,0)!=0
AND bld.bd_street_code=d.de_street_code))
-- Alternate address (free text, replaces lines 1+2)
LEFT JOIN raw_cosaltad alt ON alt.alt_alpha_key = d.de_alpha_key
WHERE (TRIM(d.de_print_code) IS NULL OR TRIM(d.de_print_code) = ''
OR TRIM(d.de_print_code) = 'N')
AND (TRIM(d.de_z_entry) IS NULL OR TRIM(d.de_z_entry) = '')
AND (TRIM(d.de_specific) IS NULL OR TRIM(d.de_specific) = '')
AND b.bn_bus_name IS NOT NULL
AND b.bn_bus_name NOT LIKE ',,%%'
AND b.bn_bus_name NOT LIKE '%%DELETE%%'
GROUP BY d.de_alpha_key, b.bn_bus_name,
bld.bd_build_name, bld.bd_abb_desig, d.de_build_no, d.de_alt_bld_no,
d.de_floor, d.de_plot_flag,
d.de_street_no, d.de_alt_str_no, s.st_street_name, s.st_abb_desig,
d.de_corner_of, alt.alt_address,
sub.sb_suburb_name, sub.sb_str_post_code,
tw.tw_town_name, tw.tw_dialing_code,
d.de_suburb_code, d.de_sch_do_not_call, d.de_amend_date,
d.de_street_code, d.de_town_code;
Agents write proposed changes back via the write layer only (Section 11). They never INSERT or UPDATE raw tables directly. All proposed changes are stored in human-readable form in verification_results, then encoded by the write layer at export time.
Agents see human-readable data (dialable phone numbers, resolved addresses, business names). They propose changes in the same human-readable form. The write layer is responsible for encoding those changes back into Informix-compatible raw format.
A step-by-step pipeline that takes Carl's ZIP dump from raw files into the PostgreSQL staging database. No flattening or transformation — raw tables mirror Informix exactly.
.txt files into corresponding raw_* tables (pipe-delimited, handle nulls)raw_cosdienr, raw_cosbusnm, raw_cosphone, raw_cosinfod, raw_cospobox, raw_cosaltadraw_costown, raw_cossubrb, raw_cosstrdc, raw_cosblddc, raw_cosregion, raw_coscountry, raw_cosplotverification_results — insert a row per de_alpha_key with record_status = 'pending'import_batchesApplied in the read layer (subscriber_view), not during import. This ensures raw data is preserved exactly as received. The filter mirrors Carl's sql.sql WHERE clause:
WHERE (de_print_code IS NULL OR de_print_code = ' ')
AND (de_z_entry IS NULL OR de_z_entry = ' ')
AND (de_specific IS NULL OR de_specific = ' ')
AND bn_bus_name NOT LIKE ',,%%'
AND bn_bus_name NOT LIKE '%%DELETE%%'
AND bn_line_no = 1
Note: Import loads ALL records into raw tables unconditionally. Exclusion filtering is applied only when agents query through the read layer. This preserves the complete Informix dataset for accurate write-back generation.
Carl will send updated file dumps periodically. Each dump contains a full export for the relevant dialing area — not a delta. The ingestion strategy must handle new records, updated records, unchanged records, and de-listed records without destroying verification history.
.txt files into temporary tables (temp_cosdienr, temp_cosbusnm, temp_cosphone, temp_cosinfod, etc.) in the same pipe-delimited format. Do not touch raw_ tables yet.alpha_key)raw_ tables. Classify every record:
alpha_key exists in temp but not in raw_ → INSERT to raw_, set record_status = 'pending'alpha_key exists in both, but one or more field values differ → UPDATE raw_, reset record_status = 'pending' (re-verification required)alpha_key exists in both, all fields identical → no-op, preserve existing verification_resultsalpha_key exists in raw_ but NOT in temp → set record_status = 'inactive' (de-listed from Informix, skip verification)tracking table. If a write-back for this alpha_key exists in tracking but has not yet been acknowledged by Carl (i.e. no confirmation that Carl has loaded it), flag the conflict and do NOT overwrite. Log it in import_batches.notes for manual review.import_batches with counts: new_count, updated_count, unchanged_count, inactive_count, conflict_count. Drop temp tables.After each import batch, only records with record_status = 'pending' enter the verification queue. UNCHANGED records are never re-queued. This keeps agent workload proportional to what actually changed in Informix — not the full dataset.
import_batches Table (Updated Schema)id SERIAL PRIMARY KEY
filename TEXT
imported_at TIMESTAMPTZ
new_count INTEGER
updated_count INTEGER
unchanged_count INTEGER
inactive_count INTEGER
conflict_count INTEGER
notes TEXT
tracking Table (Clarification)Dual purpose: The tracking table serves two roles — (1) tells Carl which alpha_keys are ready to load back into Informix production, and (2) acts as a write-back guard during subsequent imports. A record in tracking means we have pending changes for that alpha_key that Carl may not have loaded yet.
General is the dispatcher — it pulls NEW records from the database and allocates them to Claire agents in configurable batches. Claire calls each business, confirms or updates the data, and Scrub validates the updates before write-back.
subscriber_view where record_status = 'NEW' AND do_not_call = false. Concurrent dispatch: up to max_concurrent_agents (default 2) calls simultaneously. Webhook-first: after ElevenLabs webhook fires, immediately fills open slots via runDispatchCycle(). Daily orchestration at 06:00 UTC. 5-minute fallback poll as safety net. Reads campaign.agent_id to dispatch to the correct ElevenLabs agent.config/schemas/*.json), injects context into the extraction prompt, and uses schema-specific approval thresholds. Validates Claire’s updates before write-back (see Section 11)NEW → QUEUED → CALLING → QA_PENDING → [APPROVED | REJECTED | CONFLICT | HUMAN_REVIEW | FAILED | DNC] → MERGED → SYNCEDALWAYS enforced before dialling — all conditions must be true:
| Condition | Check |
|---|---|
do_not_call | Must be false (read layer derives from de_sch_do_not_call) |
record_status | Must be 'pending' |
phone | Must be NOT NULL (reconstructed by read layer) |
The do_not_call flag in subscriber_view is derived directly from raw_cosdienr.de_sch_do_not_call at query time — there is no stale copy. The read layer is the single source of truth for call eligibility.
| Status | Meaning |
|---|---|
| NEW | Imported, awaiting queue |
| QUEUED | Assigned to dial queue |
| CALLING | Claire currently on call |
| QA_PENDING | Call complete, awaiting Scrub |
| APPROVED | Scrub passed, ready for write-back |
| REJECTED | Scrub failed validation |
| CONFLICT | Conflicting data, human review needed |
| HUMAN_REVIEW | Escalated for manual review |
| FAILED | Technical failure |
| DNC | Do Not Call flagged |
| MERGED | Written back to Informix raw tables |
| SYNCED | Exported in output files for Carl |
| Outcome | Trigger | Action |
|---|---|---|
| No Answer | Call rings out, no pickup | Retry up to N times (configurable). Default: 3 attempts, spaced 24h apart |
| Voicemail | Call connects to voicemail | Do not leave message. Mark as UNREACHABLE after max retries. Log in call_log |
| Wrong Number | Business reports wrong number | Mark DNC. Log in call_log with outcome = WRONG_NUMBER |
| Busy | Line busy | Retry after 2h. Counts toward max retry limit |
| Refused / DNC | Business requests no further calls | Set de_sch_do_not_call flag. Mark record DNC permanently |
Retry rules are configured per campaign in campaign_config (to be built — see Section on Campaigns). Until campaign_config exists, defaults apply: max 3 retries, 24h spacing, no voicemail messages.
Scrub runs after Claire completes her call and captures updates. It validates what Claire captured on the call — not pre-call filtering. Scrub is a deterministic rules engine — not an AI model — that validates every field Claire modified before it reaches the write layer. Nothing goes to the write layer without passing through Scrub.
| Field | Rule | Fail Action |
|---|---|---|
| Phone | Valid SA format: 10 digits, starts with 0 (mobile) or dialcode+number (landline) |
RED — do not write back |
Valid format: contains @ and domain, no spaces |
RED — do not write back | |
| Website | Has valid TLD, reachable (optional check) | AMBER — write back with warning |
| Address | Street / suburb / town resolvable to Informix codes | If unresolvable → route to cosaltad instead |
| Business Name | No leading ,,, no DELETE, not blank |
RED — do not write back |
| Outcome | Condition | Action |
|---|---|---|
| GREEN | All fields pass | Send to write layer |
| AMBER | Soft issues only | Send to write layer with flags noted in verification_results |
| RED | Any hard failures | Block write-back, set record_status = 'failed', log reason in verification_results.agent_notes |
Full Scrub validation rules are documented in the Project Outline (Section 7).
Agents propose changes in human-readable form. The write layer encodes those changes back into Informix-compatible raw format. Unchanged records are output directly from raw_* tables with no transformation. tracking.txt is always included.
| Data Type | Encoding Rule | Target Table |
|---|---|---|
| Phone | Strip leading zero or dialing code, store as INTEGER, set ph_phone_ind accordingly |
raw_cosphone |
| Address | Lookup in raw_cosstrdc / raw_cossubrb / raw_costown; if no matching code found, write to raw_cosaltad as free text |
raw_cosdienr or raw_cosaltad |
| Email / Website | Update raw_cosinfod directly |
raw_cosinfod |
| Business Name | Update raw_cosbusnm directly |
raw_cosbusnm |
| File | Contents |
|---|---|
| tracking.txt | Always included — all alpha_keys verified with timestamps |
| cosdienr.txt | Output from raw_cosdienr; includes address code changes if any |
| cosphone.txt | Output from raw_cosphone; phone stored as integer with updated ph_phone_ind |
| cosinfod.txt | Output from raw_cosinfod; email / website updates |
| cosbusnm.txt | Output from raw_cosbusnm; business name updates |
| cosaltad.txt | Output from raw_cosaltad; non-standard addresses that couldn't be resolved to codes |
.sql schema filesraw_* tables directly — no transformationtracking.txt always included in every exportWe never invent Informix codes. If an AI-verified address doesn't match existing raw_cosstrdc / raw_cossubrb / raw_costown codes, it goes to raw_cosaltad as free text. Carl decides whether to create new codes on the Informix side.
Field-level write-back from AI verification results to raw Informix tables is live as of 11 March 2026. Four of five field types write back automatically. E2E pipeline proven with records 301, 351, 352 reaching APPROVED status. Address write-back is intentionally deferred pending Informix code lookup logic.
Scrub is the post-call processor — it runs immediately after Claire completes a call. Scrub calls Claude Haiku internally to parse the transcript and extract field-level changes, then applies deterministic validation rules (phone format, email format, address resolution). Scrub owns all writes to field_updates. Claire never writes to field_updates directly.
| Field | Target Table | Status | Notes |
|---|---|---|---|
| business_name | raw_cosbusnm |
✅ LIVE | Writes directly to bn_bus_name where bn_line_no = 1 |
| phone | raw_cosphone |
✅ LIVE | Strips leading zero / dialing code, stores as integer with correct ph_phone_ind |
raw_cosinfod |
✅ LIVE | Updates inf_data where inf_type = 'E' |
|
| website | raw_cosinfod |
✅ LIVE | Updates inf_data where inf_type = 'W' |
| address | raw_cosdienr / raw_cosaltad |
⚠ DEFERRED | Requires resolving human-readable address text to Informix code values (cosstrdc, cossubrb, costown). Flagged for manual review until code lookup logic is built. |
Address changes confirmed by Claire arrive as human-readable text (e.g. "42 Church Street, Pretoria"). To write back correctly, this must be resolved to Informix code values: a st_street_code in cosstrdc, a sb_suburb_code in cossubrb, and a tw_town_code in costown. We never invent codes — if no match is found, the address must go to cosaltad as free text and Carl decides whether to create new codes. This lookup logic is the next build item — independent of 3CX.
The pipeline currently writes verified changes back to the raw PostgreSQL tables. The final step — exporting those changes as pipe-delimited output files for Carl to load into Informix production — is not yet built. This is the next output layer to build once outbound calling is live and the write-back pipeline is validated at scale.
| Item | Status | Notes |
|---|---|---|
| 3CX SIP credentials for Claire | 🔴 ACTIVE BLOCKER | Brabys IT / Carl — no ETA. Claire uses Twilio workaround. Scheduler runs autonomously. E2E pipeline proven. |
| Snitch phone number provisioning | Pending | Snitch needs a dedicated phone number for outbound test calls to Claire. Provision via ElevenLabs dashboard — no API for buying numbers. |
| Address write-back — code lookup | Deferred | Requires code lookup tables against cosstrdc, cossubrb, costown. Address changes flagged for manual review in the interim. |
| Output file generation for Carl | Planned | Pipe-delimited export in Informix format. To be built after pilot run validates write-back pipeline end-to-end. |
| ElevenLabs billing migration | 🔴 ACTIVE BLOCKER | Claire currently on Maverix ElevenLabs account. Billing must transfer to Brabys for production scale. |
| B4 — Merge approval trigger | TBD | Manual vs scheduled nightly window — pending Brabys decision after pilot run. |
| Phone dialcode table completeness | TBD | Current data is 012 (Pretoria) only. Full costown needed for all dialing regions at production scale. |
| Full Informix table list | Confirmed | All 13 tables confirmed with Carl. All loaded into PostgreSQL. |
All 13 Informix tables with foreign-key relationships. cosdienr is the central hub — every subscriber table joins via alpha_key, and address/phone resolution flows through the lookup tables on the right.