DocsAutoResponderProcess

CUPOLA Update Statements — AutoResponder Pipeline#

Purpose#

Sai Teja (IP4) asked (2026-04-21):

"In case of CUPOLA, if Race can share me what update statements are going to be performed in a simple text file, I just double check with our team and get back to Race with what SPs (if any can be used) be called to update the changes like Make inactive, update Title/Email etc. so that Stored Procedures will take care of other fields to update automatically like Updated on, Updated by etc."

This document is the "simple text file" of every write statement the AutoResponder pipeline can issue against CUPOLA in production. It is grouped by the operation buckets Teja named in his comment. Each statement is shown with its parameter placeholders and the call-site(s) that produce it, so the DBA team can map each one to a stored procedure that would also maintain the auxiliary columns (updated_on, updated_by, audit rows, etc.).

For the currently active update paths (§1-§4), the stored procedures are already wired in code. Keep §5 as the optional/deferred mapping area if IP4 wants stored procedures for replacement auto-add rollback scenarios. The code-side swap area remains local inside src/auto_responder/services/databases/cupola.py.

Operation buckets (Teja's taxonomy)#

#OperationScopeDriver
1Make a person Inactivelink_org_person row for the sender's (person, org) pairAuto-response is inactive / retired / deceased / left_company etc.
2Update Title (w.r.t. organization)link_org_person.position for the (person, org) rowAuto-response carries a new role/title at the same org
3Update Email (w.r.t. organization)link_org_person.email for the (person, org) rowChanged Email determination for an active contact
4Replacement: mark old person inactivelink_org_person.status → 0 for the original rowreplacement determination
4Replacement: add new person (DEFERRED to IP4)CSV handoff — no CUPOLA write by the pipelineResearch team decides to override vs. insert manually

Important business rules (already enforced in code):

Per-run artifacts#

The pipeline no longer writes a per-run cupola_planned_updates.sql file. CUPOLA output is limited to the audit files (cupola_audit_log.csv / cupola_audit_log.json) and, when auto-applied status changes need a revert path, cupola_audit_log_rollback_plan.csv. Use this static document as the SP-mapping reference.

1. Make a person Inactive#

Source file: src/auto_responder/services/databases/cupola.py::execute_mark_person_inactive_sp

Trigger: action_engine._handle_inactive when the Cupola record for the sender is is_active == True and the determination is one of inactive, retired, deceased, left_company, unreachable.

sql
EXEC IP4SP_AutoBounce_MarkPersonInactive
    @OrgPersonID = :org_person_id,
    @Result      = :result_text;

Parameters bound from Python.

PlaceholderSourceExample
:org_person_idlink_org_person.org_person_id from lookup_by_email best match3241997
:result_textAction summary from pipelineAutoResponder inactive

Currently maintained automatically by this UPDATE. status.

Left to the SP (wish list from Teja's team). status_change_date, reason_comments, updated_on, updated_by. The pipeline already carries these values on the in-memory payload (reason, determination, email_source, previous_status); once the SP is known we bind them as extra parameters.

Stored procedure now used: IP4SP_AutoBounce_MarkPersonInactive

2. Update Title / Position (w.r.t. organization)#

Source file: src/auto_responder/services/databases/cupola.py::execute_org_person_updates_sp

Trigger: action_engine._handle_title_update when the determination is title_update (role change at the same organisation). Only fires when the active-only CUPOLA gate passes (preferred CUPOLA row exists and status = 1); otherwise the row is routed to Human Review and this UPDATE does not run.

sql
EXEC IP4SP_AutoBounce_OrgPersonUpdates
    @OrgPersonID = :org_person_id,
    @Position    = :new_title,
    @Email       = :email,
    @Result      = :result_text;
PlaceholderSourceExample
:new_titleDetermination's new_title field, trimmedDirector of Operations
:emailEmpty string for title-only updates``
:org_person_idlink_org_person.org_person_id3241997
:result_textAction summary from pipelineAutoResponder update

Stored procedure now used: IP4SP_AutoBounce_OrgPersonUpdates

3. Update Email (w.r.t. organization)#

Source file: src/auto_responder/services/databases/cupola.py::execute_org_person_updates_sp

Trigger: action_engine._handle_title_update (shared handler for email_update + title_update) when the determination is email_update (same person, new address). Only fires when the active-only CUPOLA gate passes — preferred CUPOLA row exists and status = 1. When the gate fails, the change routes to Human Review (HUMAN_REVIEW_REASON_UPDATE_ON_INACTIVE or HUMAN_REVIEW_REASON_ACTIVE_NEW_CONTACT) and this UPDATE does not run.

sql
EXEC IP4SP_AutoBounce_OrgPersonUpdates
    @OrgPersonID = :org_person_id,
    @Position    = :position,
    @Email       = :new_email,
    @Result      = :result_text;
PlaceholderSourceExample
:new_emailDetermination's new_email (after relay-normalization)brad.bond@morgancountyohio.gov
:positionEmpty string for email-only updates``
:org_person_idlink_org_person.org_person_id3241997
:result_textAction summary from pipelineAutoResponder update

Left to the SP (wish list). email_change_date, previous_email audit row, updated_on, updated_by.

Stored procedure now used: IP4SP_AutoBounce_OrgPersonUpdates

4. Replacement — mark old person inactive#

This is the only CUPOLA write the pipeline issues for a replacement determination under the 2026-04-20 process (see IMPLEMENTATION_PLAN §7.1). The replacement person is sent to IP4 as a CSV row for manual research.

Source file: src/auto_responder/services/databases/cupola.py::execute_mark_person_inactive_sp (same SP call as §1 — invoked against the original sender's row).

sql
EXEC IP4SP_AutoBounce_MarkPersonInactive
    @OrgPersonID = :old_org_person_id,
    @Result      = :result_text;
PlaceholderSource
:old_org_person_idorg_person_id of the original sender's row
:result_textAction summary from pipeline

The matching replacement row (new person, new email, same org) is not written by the pipeline — it's in output_document_alternate_contacts.csv with a cupola_action column set to add or update based on check_contact_exists(new_email).

Stored procedure now used: IP4SP_AutoBounce_MarkPersonInactive

Source file: src/auto_responder/services/databases/cupola.py::add_contact

This path is disabled by default and only runs when CUPOLA_AUTO_ADD_REPLACEMENTS=true. It is kept in the spec for rollback / emergency use if the team explicitly turns replacement auto-add back on.

There are three sub-paths inside add_contact:

5a. Organization lookup / create#

sql
-- Lookup first
SELECT TOP 1 organization_id
  FROM organization
 WHERE name = :org_name;

-- If not found, create
SET NOCOUNT ON;
DECLARE @out TABLE (new_id BIGINT);
INSERT INTO organization (name)
OUTPUT INSERTED.organization_id INTO @out
VALUES (:org_name);
SELECT new_id FROM @out;
sql
SET NOCOUNT ON;
DECLARE @out TABLE (new_id BIGINT);
INSERT INTO link_org_person
       (person_id, organization_id, email, position, status)
OUTPUT INSERTED.org_person_id INTO @out
VALUES (:person_id, :target_org_id, :new_email, :title, 1);
SELECT new_id FROM @out;

5c. New person (email not present anywhere in CUPOLA)#

sql
-- person row
SET NOCOUNT ON;
DECLARE @out TABLE (new_id BIGINT);
INSERT INTO person (first_name, last_name, status)
OUTPUT INSERTED.person_id INTO @out
VALUES (:first_name, :last_name, 1);
SELECT new_id FROM @out;

-- link row (same INSERT as 5b, using the person_id returned above)

Recommended SPs (TO BE FILLED IN BY TEJA'S TEAM):

6. Lookups (reference only — no writes)#

Included so Teja can verify that the SELECT clauses hitting link_org_person, person, and organization agree with his team's expectations.

No writes occur in this bucket.

7. What does not hit CUPOLA#

8. Review checklist for Teja's team#

  1. Confirm §1/§2/§3/§4 stored procedure names and parameter signatures match DBA expectations.

  2. Flag any column that the SP would populate automatically (e.g. updated_on, updated_by, status_change_date) so we know not to pass them as explicit parameters.

  3. If the SP requires inputs that are not already bound in the Python call site (e.g. a session user ID), note them so we can source them from config / the audit context.

  4. Reply on Assembla #374 or directly to rsaunders with the filled doc.

Once returned, the swap is a small edit: replace the in-place UPDATEs with cursor.execute("EXEC :sp_name ?, ?, ?, ...") calls and drop any columns the SP now handles for us.