Signals harness · target design for the new PR, nothing built yet · 2026-10-22
In one line
The plugin (code) is the question: "which competitors are on this deal?" The Signal Config row (Baserow) says for which org, how hard to push, and where the answers go. One row per org × signal. Turning a signal on, pausing it, or moving it from test columns to real ones is a row edit, not a deploy.
In code (plugin)prompt, what the model must return, the shape of the org's settings, pure decide() rules. Same for every org.
In the Signal Config rowon/off/dry-run, the org's settings, output → column + write policy, model/effort, nightly limits.
Not in itwhich data sources exist and when they're ready (Source Config) · credentials · schedule, lock, retries (core)
How a signal is named, and where its prompt lives
The Signal column holds a plugin name. The name points to a folder in the repo. Each prompt has two parts: fixed rules in code, and org-specific text in Baserow.
| competitors | field_extraction |
| Signal column value | competitors | field_extraction |
| Plugin code | services/signals/plugins/competitors/ moved from #3389's services/deal_competitors/ | services/signals/plugins/field_extraction/ wraps #3274's extractor |
| Fixed prompt (same for every org, changed by PR) | competitors/prompt.py: system instructions, the rules for what counts as a mention (testing_now, considering…), output schema, verbatim-quote rule | field_extraction/prompt.py: the wrapper that says "fill these fields from these emails/meetings, quote the evidence, return JSON" |
| Org-specific prompt text | Signal Config → Settings JSON: seller name, own products, partners, known vendors, what counts as a competitor. Inserted into the prompt at run time. | The org's Fields table (already exists): one prompt per field. Custom AI prompt wins over Ai prompt, then Prompt. Only fields with Enable AI evaluation run. Signal Config does not copy these. |
| Changing a prompt | Rules: PR + dry-run parity. Org facts: edit the Settings cell. | Wrapper: PR. A field's question: edit that field's row (CS can do this today). |
Signal Config rows never hold a whole prompt. Prompt rules stay versioned and tested in code. The org-specific parts are data the prompt reads. Each Runs row records the prompt version and config version it used.
The columns, with two real examples
| Column | What it means | Onyx · competitors | Qu · field_extraction |
| Org, Signal | who, and which plugin | Onyx (ws1197) · competitors | Qu · field_extraction |
| Mode | off / dry-run / live (below) | dry-run first | dry-run (the 1-week shadow) |
| Settings (JSON) | org facts the prompt and rules need. Checked against the plugin's schema; a bad row doesn't run. | seller name, own products, partners, known vendors, what counts as a competitor, closed stages | which mailboxes count (e.g. Deal Driver users only), lookback for a new org (e.g. 180 days) |
| Sources (optional) | which sources must be complete before it decides. Empty = all sources the plugin reads. | meetings + emails (Slack can lag without blocking) | emails |
| Outputs (JSON) | each named answer from decide() → a Baserow destination + a write policy | health → Competition Status (test) · card
description → Competition Description (test) · card
reasoning → Competition Reasoning (test) · card
rows → OpptyCompetitors · rows | fields → the org's AI-enabled CRM fields. Policy comes per field, by field type, from the Fields table, not listed here (e.g. ws1323 today: 38 if-empty, Next Step overwrite, 1 review) |
| Model | model + effort for this org × signal | e.g. high effort | e.g. medium |
| Limits | max subjects per night, $ cap per night. Hitting a cap stops cleanly; the rest go first next night. | e.g. 10 deals while testing | e.g. $ cap from the shadow cost |
What each mode does
| Mode | Model runs? | Writes to Baserow / CRM? | Use it for |
off | no | no | not onboarded, or paused |
dry-run | yes | no. Logs what it would write in the Runs row | new org, new prompt, parity checks against the old PRs |
live | yes | yes, per each output's policy | after the dry run looks right |
Emergency stop is separate: the Redis kill switch halts runs already in flight. Mode only affects the next dispatch.
How a run uses the row
Dispatch reads every row whose mode isn't off.
Validate Settings and Outputs against the plugin. Unknown column, wrong JSON, missing setting → that org × signal is skipped and its Runs row says failed: config invalid. Nothing is written.
Snapshot the row at the start. An edit mid-run takes effect next run, so one run never mixes two configs. The Runs row keeps which config version it used.
Run each subject (lock, completeness against Sources, changed items only, model with Model, quote check, decide()).
Publish: each output goes to its destination with its policy. In dry-run this step only records.
Limits are checked between subjects. No alerts: the result is the field update, seen on the dashboard.
Write policies (the per-output choice)
| Policy | Writes | If a person edited it | Reaches CRM? |
card | AI-owned Deals columns, every run | n/a, the column belongs to the signal | never |
rows | AI-owned child rows, one per key (deal × competitor) | rows a person made or hid are left alone | never |
if-empty | a field, only while it's empty | never overwritten | yes |
overwrite | a field, whenever the answer changes | held, not overwritten | yes |
review | the field's AI Recommended column | untouched | only after a person accepts |
Still yours to decide (affects these rows)
| Outputs | Onyx competitors: Oliv only (card + rows), or also a Salesforce field? If SF: review or if-empty? |
| Outputs | When do the (test) columns switch to the real Competition card? (one Outputs edit) |
| Model | One OpenAI key or project per signal, so spend shows per signal? (would add a Key column) |