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.
competitorsfield_extraction
Signal column valuecompetitorsfield_extraction
Plugin codeservices/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 rulefield_extraction/prompt.py: the wrapper that says "fill these fields from these emails/meetings, quote the evidence, return JSON"
Org-specific prompt textSignal 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 promptRules: 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

ColumnWhat it meansOnyx · competitorsQu · field_extraction
Org, Signalwho, and which pluginOnyx (ws1197) · competitorsQu · field_extraction
Modeoff / dry-run / live (below)dry-run firstdry-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 stageswhich 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 policyhealth → 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)
Modelmodel + effort for this org × signale.g. high efforte.g. medium
Limitsmax subjects per night, $ cap per night. Hitting a cap stops cleanly; the rest go first next night.e.g. 10 deals while testinge.g. $ cap from the shadow cost

What each mode does

ModeModel runs?Writes to Baserow / CRM?Use it for
offnononot onboarded, or paused
dry-runyesno. Logs what it would write in the Runs rownew org, new prompt, parity checks against the old PRs
liveyesyes, per each output's policyafter 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

  1. Dispatch reads every row whose mode isn't off.
  2. 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.
  3. 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.
  4. Run each subject (lock, completeness against Sources, changed items only, model with Model, quote check, decide()).
  5. Publish: each output goes to its destination with its policy. In dry-run this step only records.
  6. Limits are checked between subjects. No alerts: the result is the field update, seen on the dashboard.

Write policies (the per-output choice)

PolicyWritesIf a person edited itReaches CRM?
cardAI-owned Deals columns, every runn/a, the column belongs to the signalnever
rowsAI-owned child rows, one per key (deal × competitor)rows a person made or hid are left alonenever
if-emptya field, only while it's emptynever overwrittenyes
overwritea field, whenever the answer changesheld, not overwrittenyes
reviewthe field's AI Recommended columnuntouchedonly after a person accepts

Still yours to decide (affects these rows)

OutputsOnyx competitors: Oliv only (card + rows), or also a Salesforce field? If SF: review or if-empty?
OutputsWhen do the (test) columns switch to the real Competition card? (one Outputs edit)
ModelOne OpenAI key or project per signal, so spend shows per signal? (would add a Key column)