Maverix Studio
Technical Specification
Brabys
Data Pipeline
Informix → PostgreSQL · AI Verification Staging Layer
ClientBrabys Directory Services
AuthorMaverix Studio
DateMarch 2026 (revised)
Versionv0.5
Section 01

Overview

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.

Pipeline Status — 11 March 2026

🟢 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.

13
Source Tables
21,872
Subscribers Loaded
4
AI Agents
6
Output Files

Architecture

INBOUND CARL / INFORMIX Carl's AIX server FILE DUMP ZIP · pipe-delimited IMPORT Parse & load raw POSTGRESQL Staging DB raw tables only PROCESSING READ LAYER Views / API GENERAL Dispatch & allocate CLAIRE Voice verification SCRUB Validate updates OUTBOUND WRITE LAYER Encode to Informix OUTPUT FILES Pipe-delimited export CARL / INFORMIX Production load

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.

Important

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.

Section 02

Source Data Format

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.

Source Tables

Each dump contains the following 13 tables:

TablePurpose
cosdienrCore subscriber record — address codes, flags, primary key
cosbusnmBusiness name(s) per subscriber
cosphonePhone numbers (multiple per subscriber)
cosinfodEmail addresses and website URLs
cospoboxPO Box address
cosaltadFree-text alternate address (non-standard)
cosstrdcStreet code lookup
cossubrbSuburb code lookup
costownTown/city lookup (includes dialing code)
cosblddcBuilding code lookup
cosregionRegion lookup
coscountryCountry lookup
cosplotPlot type lookup

Data Profile — Loaded as of 11 March 2026

Pretoria / 012 dialing area — fully loaded into PostgreSQL staging:

21,872
Subscribers
36,250
Phone Entries
25,313
Email / Web Entries
13
Tables per Dump

File Format

de_alpha_key|de_town_code|de_suburb_code|de_street_code|de_street_no|...
1234567|012|4501|88210|42|...
Section 03

Key Field Reference — cosdienr

The 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.

Core Fields

FieldTypeDescription
de_alpha_keyINTEGERPrimary key, immutable — links all tables
de_town_codeFKForeign key → costown (tw_town_code)
de_suburb_codeFKForeign key → cossubrb (sb_suburb_code)
de_street_codeFKForeign key → cosstrdc (st_street_code)
de_build_codeFKForeign key → cosblddc (building name)
de_street_noINTEGERStreet number
de_alt_str_noCHAR(1)Letter suffix for street number (e.g. a in 12a Caversham Road)
de_build_noINTEGERBuilding / unit number
de_alt_bld_noCHAR(6)Letter/suffix for building number (e.g. b in 27b Sanlam Building)
de_floorCHAR(2)Floor (for buildings)
de_corner_ofVARCHARCross street text (e.g. Underwood Road &). Code prepends Cnr. — main street resolved via cosstrdc
de_amend_dateDATELast amended date (dd/mm/yyyy)

Filter & Status Flags

FieldTypeDescription
de_print_codeCHARPrint filter flag — NULL / " " = printable
de_z_entryCHARExclude flag — NULL / " " = include
de_specificCHARSpecific-type entry flag
de_unlistedCHARUnlisted flag
de_sch_do_not_callCHARDo Not Call flag — MUST be checked before any outbound call
Exclusion Rules

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.

Section 04

Phone Number Reconstruction

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.

Phone Type Indicators

Two fields determine the phone type: ph_phone_ind and ph_fax_ind.

Phone Types (ph_phone_ind)

CodeTypeReconstruction
"Y"Phone (landline)Prepend tw_dialing_code from costown
"C"Cell / MobileSouth Africa: prepend "0"; otherwise as-is
"V"VOIPSouth Africa: prepend "0"; otherwise as-is
"S"SharecallSouth Africa: prepend "0"; otherwise as-is
"T"Toll-freeSouth Africa: prepend "0"; otherwise as-is

Fax Types (ph_fax_ind)

CodeTypeReconstruction
"Y"FaxPrepend tw_dialing_code
"C"CellFaxSouth Africa: prepend "0"
"V"VOIPFaxSouth Africa: prepend "0"
"X"ShareCallFaxSouth Africa: prepend "0"

Dialing Code Join Path

cosphone.ph_alpha_key = cosdienr.de_alpha_key
  → cosdienr.de_town_code = costown.tw_town_code
  → use costown.tw_dialing_code

Examples

Landline: ph_phone = 123456, tw_dialing_code = "012" → reconstructed = "012123456"

Mobile: ph_phone = 821234567, ph_phone_ind = "C", country = SOUTH AFRICA → "0821234567"

Call Eligibility

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.

Section 05

Address Resolution

Street addresses are assembled from code lookups as defined in address.4gl (get_street_address function). Alternate free-text addresses take priority when present.

5-Line Address Model

Addresses are assembled as five lines, with empty lines omitted:

LineComponentsSource
1 — Buildingbd_build_name + bd_abb_desig + de_build_no + de_alt_bld_no + de_floor + de_plot_flagcosblddc + cosdienr
2 — Streetde_street_no + de_alt_str_no + st_street_name + st_abb_desig; if de_corner_of present, prepend “Cnr.”cosdienr + cosstrdc
3 — Suburbsb_suburb_namecossubrb
4 — Towntw_town_namecostown
5 — Postal Codesb_str_post_code (or country if none)cossubrb

cosaltad override

cosaltad.alt_address replaces Lines 1 + 2 only. Lines 3–5 (suburb, town, postal code) always come from codes.

cosblddc conditional join

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.

Write-Back Rule

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.

Section 06

PostgreSQL Staging Schema

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.

Raw Tables — Subscriber Data

Exact column-for-column mirrors of the Informix originals. One raw_ table per source file:

TablePurposeJoin Key
raw_cosdienrCore subscriber record — address codes, flags, PKde_alpha_key (PK)
raw_cosbusnmBusiness name(s) per subscriberbn_alpha_keyde_alpha_key
raw_cosphonePhone numbers (multiple per subscriber)ph_alpha_keyde_alpha_key
raw_cosinfodEmail addresses and website URLsinf_alpha_keyde_alpha_key
raw_cospoboxPO Box addresspo_alpha_keyde_alpha_key
raw_cosaltadFree-text alternate address (non-standard)alt_alpha_keyde_alpha_key

Raw Tables — Lookups (Read-Only)

Reference tables used by the read layer to resolve codes into human-readable values. Never modified by agents:

TablePurposePrimary Key
raw_costownTown / city lookup (includes dialing code)tw_town_code
raw_cossubrbSuburb code lookupsb_suburb_code
raw_cosstrdcStreet code lookupst_street_code
raw_cosblddcBuilding code lookupbd_build_code
raw_cosregionRegion lookuprg_region_code
raw_coscountryCountry lookupcy_country
raw_cosplotPlot type lookuppl_plot_code

Operational Tables

import_batches

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
);

verification_results

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
);

field_updates

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
);
Write-Back Source of Truth

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.

tracking

Handshake table written back to Carl — tells Informix which records are ready to load.

CREATE TABLE tracking (
  alpha_key    INTEGER,
  verified_at  TIMESTAMPTZ
);

campaigns

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
);

config/schemas/

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/templates/

Campaign templates — pre-configured campaign definitions used by the Campaign Creation Wizard. Each template pre-fills campaign settings from a JSON file.

Design Principle

Raw 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.

Section 07

Read Layer — Agent Interface

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.

What the Read Layer Resolves

subscriber_view

Primary 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;

Write Path

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.

Key Rule

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.

Section 08

Import Process

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.

Pipeline Steps

  1. Receive ZIP from Carl → extract to staging folder
  2. Run import script — loads all .txt files into corresponding raw_* tables (pipe-delimited, handle nulls)
  3. Load all 13 tables:
    • 6 subscriber tables → raw_cosdienr, raw_cosbusnm, raw_cosphone, raw_cosinfod, raw_cospobox, raw_cosaltad
    • 7 lookup tables → raw_costown, raw_cossubrb, raw_cosstrdc, raw_cosblddc, raw_cosregion, raw_coscountry, raw_cosplot
  4. Initialise verification_results — insert a row per de_alpha_key with record_status = 'pending'
  5. Log import batch to import_batches

Exclusion Filter

Applied 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.

Section 09

Batch Ingestion Strategy

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.

Strategy: Upsert + Change Detection

  1. STEP 1 — Load to temp tables
    Load all incoming .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.
  2. STEP 2 — Compare and classify (by alpha_key)
    Compare temp tables vs raw_ tables. Classify every record:
    • NEW: alpha_key exists in temp but not in raw_ → INSERT to raw_, set record_status = 'pending'
    • UPDATED: alpha_key exists in both, but one or more field values differ → UPDATE raw_, reset record_status = 'pending' (re-verification required)
    • UNCHANGED: alpha_key exists in both, all fields identical → no-op, preserve existing verification_results
    • INACTIVE: alpha_key exists in raw_ but NOT in temp → set record_status = 'inactive' (de-listed from Informix, skip verification)
  3. STEP 3 — Pending write-back guard
    Before overwriting any UPDATED record, check the 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.
  4. STEP 4 — Log the batch
    Write a record to import_batches with counts: new_count, updated_count, unchanged_count, inactive_count, conflict_count. Drop temp tables.

Re-verification Queue

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.

Section 10

AI Verification Flow

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.

Verification Pipeline

  1. General (dispatcher) pulls records from 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.
  2. Claire (ElevenLabs voice, ZA female) calls the reconstructed phone number and speaks with the business to confirm or update: phone, email, address, business name
  3. Call outcome recorded: APPROVED (data confirmed correct) | UPDATED (data changed) | UNREACHABLE (no answer, wrong number) | DNC (do not call)
  4. Scrub is now schema-aware: loads the campaign schema (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)
  5. General tracks status through the full lifecycle: NEW → QUEUED → CALLING → QA_PENDING → [APPROVED | REJECTED | CONFLICT | HUMAN_REVIEW | FAILED | DNC] → MERGED → SYNCED

Call Eligibility Check

ALWAYS enforced before dialling — all conditions must be true:

ConditionCheck
do_not_callMust be false (read layer derives from de_sch_do_not_call)
record_statusMust be 'pending'
phoneMust be NOT NULL (reconstructed by read layer)
Critical

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 Reference

StatusMeaning
NEWImported, awaiting queue
QUEUEDAssigned to dial queue
CALLINGClaire currently on call
QA_PENDINGCall complete, awaiting Scrub
APPROVEDScrub passed, ready for write-back
REJECTEDScrub failed validation
CONFLICTConflicting data, human review needed
HUMAN_REVIEWEscalated for manual review
FAILEDTechnical failure
DNCDo Not Call flagged
MERGEDWritten back to Informix raw tables
SYNCEDExported in output files for Carl

Retry & Voicemail Logic

OutcomeTriggerAction
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 Configuration

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.

Section 11

Scrub — Validation Layer

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.

Validation Rules

FieldRuleFail Action
Phone Valid SA format: 10 digits, starts with 0 (mobile) or dialcode+number (landline) RED — do not write back
Email 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 Logic

OutcomeConditionAction
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
Cross-Reference

Full Scrub validation rules are documented in the Project Outline (Section 7).

Section 12

Write-Back Format

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.

Write-Layer Encoding

Data TypeEncoding RuleTarget 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

Output Files

FileContents
tracking.txtAlways included — all alpha_keys verified with timestamps
cosdienr.txtOutput from raw_cosdienr; includes address code changes if any
cosphone.txtOutput from raw_cosphone; phone stored as integer with updated ph_phone_ind
cosinfod.txtOutput from raw_cosinfod; email / website updates
cosbusnm.txtOutput from raw_cosbusnm; business name updates
cosaltad.txtOutput from raw_cosaltad; non-standard addresses that couldn't be resolved to codes

Format Rules

Key Principle

We 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.

Section 13

Write-Back Flow — Implemented

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.

Write-Back Pipeline

Call Ends
Call completes
Claire writes call_log + sets QA_PENDING
Call record + transcript saved
Scrub (Claude Haiku)
Parses transcript, validates fields
field_updates
Stores proposed field changes
Raw Table Update
Writes back to Informix raw tables in PostgreSQL
Output Files
For Carl to load into Informix production (to be built)

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 Write-Back Status

FieldTarget TableStatusNotes
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
email 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 Write-Back — Why Deferred

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.

Output Files for Carl — Status

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.

Open Items

ItemStatusNotes
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.
Appendix A

Informix Entity-Relationship Diagram

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.

SUBSCRIBER TABLES HUB LOOKUP TABLES cosbusnm bn_alpha_key FK bn_bus_name bn_line_no bn_sort_name cosphone ph_alpha_key FK ph_phone INTEGER ph_phone_ind ph_fax_ind ph_sequence cosinfod inf_alpha_key FK inf_data inf_type inf_sequence cospobox po_alpha_key FK po_box_no po_box_type cosaltad alt_alpha_key FK alt_address alt_line_no cosdienr de_alpha_key PK de_town_code FK de_suburb_code FK de_street_code FK de_build_code FK de_street_no de_build_no de_floor de_print_code de_z_entry de_specific de_sch_do_not_call de_amend_date costown tw_town_code PK tw_town_name tw_dialing_code tw_country FK tw_region_code FK cossubrb sb_suburb_code PK sb_suburb_name sb_town_code FK sb_postal_code cosstrdc st_street_code PK st_street_name st_suburb_code FK cosblddc bd_build_code PK bd_build_name bd_street_code FK cosregion rg_region_code PK rg_region_name coscountry cy_country PK cy_country_name cosplot pl_plot_code PK pl_plot_name pl_plot_type bn_alpha_key ph_alpha_key inf_alpha_key po_alpha_key alt_alpha_key de_town_code de_suburb_code de_street_code de_build_code tw_region_code tw_country sb_town_code LEGEND Hub table (cosdienr) Subscriber / Lookup table Foreign key relationship PK = Primary Key FK = Foreign Key