Outbound after OUTBOUND-IN-OFFICE.md: the same three rules (one record in Postgres with consent at the root ·
one clock — the contact's · one policy, cadence.yaml through can_send()), with the stage machine, the
enrolment rule, a capacity block, a content library, and SIE Data consumed as a provider behind a key. Nothing here
relitigates the Chairman's decisions:
keep airtable in the archive for what it currently has but all its content and moving forward should be custom
in the office with the postgres · warm only until GO · GO = 2026-09-13 · the marketing calendar and cockpit [should]
be completely separate from the outbound … the only connection between inbound and outbound is somewhere within
SEO · [intent] should live within the outbound · we need SIE for API calls and the data. we can do the actual outbound
| Marketing | Outbound | |
|---|---|---|
| unit | asset · cell · cut | contact · account · touch |
| clock | the fiscal calendar (publish date) | the contact's timeline (days since enrolment, capacity/day, a reply) |
| editor | authoring workbench + the calendar twin | a queue: approve a draft, read a reply, move a stage |
| owner seat | creative / EM | reply seat / ED |
| policy | brand rails, likeness consent | cadence, consent receipts, GO |
| result | per asset on the calendar | per contact per sequence |
The only joins are registries: the record (with enroll_from, never a door), the segment and its door (UTM medium),
the intent registry inside outbound, and content as a library. Each is named below where it applies.
| Source | Enters as | Consent | Stage on arrival | Enrolment source (plan_outbound.enroll_source_of) | May be enrolled? |
|---|---|---|---|---|---|
a door on temerarii.xyz (/go/<segment>, /inquire, /book/<event>) | crm_contacts + inquiries / bookings + a crm_touches row (door · inquiry · booking) | a versioned receipt, at the root | warm | door | never — the inbound loop owns them (not_enrollable:door) |
| the archive (136 rows, 54 "met at an event") | imported once, consent.receipt = null | none until the re-consent send | imported | imports-without-receipt | not in a sequence; the ONE re-consent send is a scheduled batch (plan_sends) |
| the archive, after the re-consent answer | the same row, receipt now present | the re-consent receipt | warm | imports-with-receipt | yes |
| a referral / a warm intro a seat adds, in writing | crm_contacts.source = manual + a note touch | the receipt the seat records | warm once the receipt exists | manual | yes |
| a SIE signal on an account (a company surging on a topic) | crm_touches kind=signal on every contact of that domain in the segment | none — a signal is an account fact, not permission | no change | signals (only on a contact with a receipt and no door) | yes, in SIE-served segments only (intent.yaml enroll) |
| a partner / a channel list | never imported without a receipt | — | — | — | — |
policy.outbound.enroll_from = ["signals", "imports-with-receipt", "manual"], _never: ["door"], with the Chairman's
sentence as _who. The intent registry may narrow the list per segment (a segment SIE does not serve carries no
signals), never widen it. The lead-source is recorded, never inferred; a source that carries no receipt never
becomes sendable by being imported.
governance/outbound.json stages — a contact is in exactly one stage, derived from the record by
plan_outbound.stage_of() and never set by hand:
suppressed a cadence.yaml suppress state on the consent root no send, ever, until it changes
customer a held booking, or crm_contacts.stage = customer customer-<segment>: expand → referral
warm a consent receipt warm-<segment>: value → proof → book
imported an archive row, receipt null the re-consent batch (plan_sends) — never a sequence step
cold nothing cold-<segment>: declared, CLOSED (policy.outbound.cold_lane)
Exits: a receipt or a reply moves cold → warm; a booking or a won opportunity moves warm → customer; unsubscribed /
bounced / complained moves anything → suppressed; churn moves customer → warm. A reply closes the open sequence:
plan_outbound skips a contact whose latest touch on the sequence is a crm_touches kind=reply (open_reply()), and
the reply seat answers inside capacity.reply_seat_sla_hours. A step reaches its own stage only.
Sequences per segment × stage (governance/outbound.json v2, 40 sequences · 87 steps, built from gtm.yaml × funnel.yaml
× cadence.yaml, never edited by hand; no week, no placement, no mirror of any calendar):
| stage | sequence | steps | channel | content |
|---|---|---|---|---|
| imported | reconsent-warm (segment *) | 1 | outbound_email | static — the receipt ask, the one link to the form; scheduled by policy.outbound.sends, not by a sequence clock |
| warm (the three funnel tracks) | warm-compliance-exposed · warm-broken-attribution · warm-neutral-builder | 6 · 6 · 5 | outbound_email (the last neutral-builder step is a linkedin_dm) | the funnel arc: pillar steps alternate value / proof, the last is the book step (static) |
| warm (the other ten) | warm-<segment> | 3 | outbound_email | value (pillar) at +0 · proof (pillar) at +4 · book (static) at +8 — cadence.yaml lanes.nurture.interval_days |
| customer | customer-<segment> | 2 | outbound_email | expand (pillar, the adjacent pillar) at +0 · referral (static) at +14 |
| cold | cold-<segment> | 1 | outbound_email | the short ask; active: false while policy.outbound.cold_lane is closed |
Every step carries `{n, offset_days, channel, template, purpose, purpose_kind, door, door_label, kind, content_from:
pillar | static}. The gate refuses a stage the machine does not know, a content_from` outside the two, a
purpose_kind pick_content cannot resolve, a segment with a missing stage sequence, and a cold sequence that is active.
A step is due at enrolled_at + offset_days. enrolled_at is the contact's earliest held / draft / sent touch on the
sequence (crm_touches.payload.date, else at); a contact with none is a new enrolment whose day 0 is the
planning week's Tuesday. plan_outbound --week <key> walks every enrollable contact (ordered by intent_score, the
warmest first), finds the steps due inside that fiscal week, and runs each through decide(): the stage machine, the
open-reply rule, the receipt rule, the daily cap applied ahead of time, then cadence.can_send() with the contact's
real state at the send moment (10:00 local). The week_key on a row is bookkeeping for capacity and the queue — it is
not a placement on anything, and nothing in the calendar knows it exists.
2026W37 dry-run (the GO week, over the archive preview): 136 contacts, all imported → `not_enrollable:imports-without-
receipt 136 · not_enrollable:door 0` · 0 held. Honest: nobody has a receipt yet; the re-consent batches below ask for
one. The first warm enrolments come the week after the first answers.
crm_contacts.priorities holds the five Typeform multi-selects as `{marketing · automation · development · creative ·
relations: [choices]}. pick_content maps each choice to one of the eight content pillars (PRIORITY_PILLARS`: paid /
SEO / email / lead gen → performance · social media → social · branding → brand · content production → multimedia ·
CRM / workflows / integrations → it_dev · partnerships / PR → strategic_relations · "not sure" → emerging_tech) into an
ordered list, then for every content_from: pillar step of the contact's sequence picks a published piece from
the library on that pillar — newest first, a value step preferring a blog or long-form, a proof step a thread (a
receipt with a number), an expand step the next pillar; a piece is never repeated for the same contact.
The library is what the public can open, and nothing else:
| source | published when | URL |
|---|---|---|
content/blog/*.md (frontmatter title · slug · date · pillar) | date ≤ today and apps/lab/public/blog/<slug>/index.html exists | https://temerarii.xyz/blog/<slug>/ |
the content index (remotion/src/content-index.json cells + threads) | status: published, or the week is in published-weeks.json and a CDN rendering exists | the CDN URL |
The row is held with content_ref (blog:<slug> or the index id) and content_url; draft_touches composes the
letter from the piece's title and excerpt inside the channel spec's caps and the brand rails, the studio's name
rewritten out. The letter keeps one link — the door (the outbound_email spec); the piece's URL rides on the row
for the reviewer and the cockpit queue. A contact with no priorities takes the segment's own pillar (gtm.yaml) and the
row says pillar_source: segment. No calendar week is read, ever; verify_outbound greps for it.
Today the library holds 7 published articles (2026W27) and every one is unclassified — the frontmatter carries no
pillar: — so pick_content reports them and picks nothing (no_published_piece:<pillar> on a live pillar step).
Marketing adds the pillar line; outbound infers nothing. --include-drafts --assume-warm previews the mechanism over
the 1,428 draft index items: 54 pillar steps over 136 contacts resolve to 28 long-forms and 26 threads, 0 refusals.
governance/cadence.yaml capacity (mirrored into the registry; the gate holds them equal):
capacity:
touches_per_seat_per_day: 25 # drafts a seat reviews (and, after GO, sends) in a day
reply_seat_sla_hours: 24 # a reply is answered by a person inside this window
max_active_sequences_per_seat: 150 # contacts mid-sequence one seat can hold
seats: { outbound: [executive-director], reply: [executive-director] }
channels:
linkedin: { lanes: [nurture], max_chars: 300, no_link_in_first_message: true, questions_per_message: 1,
per_seat_per_day: 20, min_days_between_dms: 7, quiet_hours: inherit }
plan_outbound caps a day's held rows at touches_per_seat_per_day × outbound seats and a week's distinct contacts
mid-sequence at max_active_sequences_per_seat × seats; plan_sends caps each scheduled batch the same way. The
excess is refused as capacity_day / capacity_active and counted, never silently dropped. That is why the
re-consent send is five batches: 119 addressable contacts ÷ 5 = 24 · 24 · 24 · 24 · 23 a day under a 25/day cap on one
seat (plan_sends --all, 2026-09-15 … 19).
| scales with headcount (a seat) | scales with the machine (free) |
|---|---|
| reviewing drafts (25 / seat / day) | planning: every candidate through can_send(), the stage machine, the clock |
| answering replies (the 24 h SLA) | the library lookup, drafting, the rails, the caps |
| LinkedIn DMs (a person types each one; 20 / seat / day) | the gates, the office pages, the metrics fold |
| the rationale for touching an archive row | enrichment: the crawler, SIE, provenance per field |
| opening the cold lane, in writing, with a date | signals: pulled, matched, scored |
Adding a seat to capacity.seats.outbound doubles the daily ceiling and nothing else changes.
Boundary (the Chairman, 2026-08-25): "we need SIE for API calls and the data. we can do the actual outbound."
SIE is data and API; the office does the outbound on its own Postgres record and email infra. Nothing in SIE
sends on our behalf, and nothing in SIE decides who is enrolled.
SIE Data (C:\Users\temer\sovereign-intent-engine, app.siedata.dev) stays its own product. The office reaches it over
HTTP from ONE module, engine/integrations/sie.py, and never ports its code. What the SIE repo actually exposes:
| base | https://api.siedata.dev/api/v1 — SIE_API_BASE overrides |
| auth | X-API-Key: <buyer key> → verify_api_key (signals · pricing · credits). Authorization: Bearer <jwt> → get_current_user — the /enrich routes are user-authenticated today; SIE_API_KEY carries the first, SIE_API_TOKEN the second when the Chairman issues one. The confirmed contract (docs/handoffs/sie-buyer-key.md): one Temerarii key with three scopes — enrichment · signals listing · scoring |
| enrich | POST /enrich/{field} — field ∈ email · find_email · phone · company · tech_stack · verify_email · intent → key path {provider, cost:{credits,usd}, fields:{field: value}, sources} · POST /enrich/batch · GET /enrich/providers |
| signals | GET /signals/surges?topic=&intensity=&min_confidence=&limit= (company topic surges) · GET /signals/batch?industry=&limit=&offset= · POST /signals/quote → POST /signals/purchase — the paid act; never called |
| intent | POST /scoring/score {record, industry, persist} → the office keeps intent_classification, discards the score |
| metering | enrichment 5 credits per field call (1 BYOK); tiers base 1 · contact 5 · deep 50; Pro $0.198/credit, agency $0.10; signal listing 0 credits, a surge purchase $3.00; /scoring/score declares no charge. VERIFIED against GET /pricing/tiers before any spend |
| the rule | signals, not scores — sie.FCRA_BLOCKED is the one place the eight names are written; assert_allowed() refuses a request, scrub() drops them from every response, and the gate proves no other enrichment file names one |
Every call: refuses without SIE_API_KEY, prints the estimated cost first, returns `{value, source: "sie:<provider>",
at, cost: {credits, usd}} per field. python -m engine.integrations.sie --probe` prints exactly what a run would call.
Which segments SIE serves, and what it is asked — two registries, no code: gtm.yaml icp.segments[].engine == "sie"
names the served segments (compliance-exposed · publishers · data-brokers · directories · gtm-consolidators ·
agency-resellers); governance/outbound/intent.yaml segments.<id>.sie_topics names the topics sie.signals() lists
surges for (sie.topics_for(); there is no map in sie.py any more, and the gate refuses one). A served segment the
registry gives no topics is refused; an unserved segment is refused; pull_signals_sie asks served_segments() and
never carries a list of its own. The topic names are SIE's TOPIC_TAXONOMY keys (20, listed under taxonomy.names,
verified: false until GET /public/signals/taxonomy is read with the key).
A signal lands as crm_touches kind='signal' with payload `{signal_type, strength, source, observed_at, topic,
company_domain, sie_id, segment_id} on every contact of that domain in that segment; score_intent` weighs it by the
segment's signal_weight from the registry (default 15, capped at 2 signals); plan_outbound may enrol on it
(enroll_from: signals) only where the registry allows and only a contact with a receipt and no door.
X-API-Key on /enrich/{field} and /enrich/batch through the existing verify_api_key, charging the buyer's credits (SIE Batch A, in progress).
since filter on signals — /signals/surges and /signals/batch page by limit/offset; the office filters client-side on observed_at | detected_at | created_at until a server-side since exists.
GET /signals/surges?topics=a,b would halve the calls per segment; the office's topics per segment live in intent.yaml and are verified against GET /public/signals/taxonomy.
crm_accounts.enrichment (20260831_contact_graph.sql: one row per domain, {<field>: {value, source, at, cost}},
"enrichment attaches here, never on the person"). enrich_contact writes the account row and links the contacts'
account_id. Order per domain: our crawler (free) → SIE (company, tech_stack) → other paid providers only when a
key exists. enrich_contact is consent-first: only a contact with a receipt or a source that is one of our own doors
is enriched, and an archive-only row is skipped unless a person types the rationale.
python -m engine.integrations.sie --probe # what SIE would be asked; no network
python -m engine.stages.import_airtable_archive [--live] # the archive → the record, receipt null
python -m engine.stages.assign_segment [--live] # segment_rules.json via crm.segment_for
python -m engine.stages.enrich_contact [--limit N] [--live --yes] # consent-first; crawl → SIE (cost printed) → others
python -m engine.stages.pull_signals_sie [--since ISO] [--live] # SIE-served segments → crm_touches kind=signal
python -m engine.stages.score_intent [--live] # intent-v2, the formula printed
python -m engine.stages.plan_sends --all # the scheduled batches → sends-<date>.json, held
python -m engine.stages.plan_outbound --week 2026W37 [--live] # enroll_from · the contact's clock · capacity · can_send() → held
python -m engine.stages.pick_content --week 2026W37 [--live] # pillar steps → a published piece (content_ref + URL)
python -m engine.stages.draft_touches --week 2026W37 [--live] # the letter, the caps, the rails → draft
python engine/gates/verify_outbound.py # OUTBOUND PASS
python engine/gates/verify_intent_registry.py # INTENT-REGISTRY PASS
python apps/office/render_outbound.py # /outbound/*: overview · sequences · sends · signals · enrichment · intent · replies · policy
python -m engine.stages.pull_metrics_outbound --live # metrics_daily channel outbound → /reports
Every stage is dry-run by default and refuses cleanly without SIE_API_KEY or the Postgres env; the send is not in
this repository.
intent-v2: score = round( (min(door_hits,3)×10 + inquiry_answered×25 + booking_held×40 + replied×30
+ min(sie_signals,2)×signal_weight + gsc_overlap×10) × 0.5^(days_since_last_signal/30) )
door_hits over 90 days · sie_signals = crm_touches kind signal over 90 days with payload.strength ≥ 0.5 (SIE-served
segments only) · signal_weight = governance/outbound/intent.yaml segments.<id>.signal_weight (default 15) · NULL when no
input · range 0–165 at the default weight (v1 scores are comparable up to 105)
.env SIE_API_KEY (and a service token → SIE_API_TOKEN until SIE accepts the buyer key on /enrich). Every SIE stage refuses until then and prints what it would have called.
governance/intake/forms.json) — form:inquiry is the fallback door until then.