Multiple connections per plugin
One person, two accounts on the same connector — work and personal. Today the second connection does not add a choice, it removes one: the plugin stops resolving, and every inbound event fires every trigger the owner has. This is the flow, what breaks in it, and the two decisions left.
Where an event goes today
Today
both accounts matchEvery provider normalizer already stamps the connection onto the event row. The matcher never reads it, so the two lanes converge and are indistinguishable by the time a run is enqueued.
Proposed
pinned account passesThe trigger records which connection it listens to. The matcher compares it to the connection already on the event, and the other account stops one step short of a run.
work connection personal connection animation loops; static for reduced-motion readers
What is already in place
| Piece | Where | State |
|---|---|---|
| Two live connections per user per connector | connections_user_connector_unique is on (org, connector, user, external_account_id) |
Storable today |
| Connection stamped on every event | automationEvents.integrationConnectionId, set by every normalizer — Linear, Slack, Notion, Sentry, Teams, Gmail |
Recorded, never read for matching |
| Per-connection pin on the MCP proxy | ?connection=<id> → pinnedConnection() in resolve-target.ts |
Works; nothing ever sets it |
| Connections as a list in the API | plugins.list returns connections[] and accounts[] |
Shape already anticipates several |
| Chips in the trigger sentence | TriggerSentence with ScopeChip, SelectChip |
An account chip fits the existing grammar |
What breaks
-
Resolution refuses to choose.
userConnection()reads two rows andsingle()throwsAmbiguousConnectionError— “disconnect the one you do not want.” So connecting a second account does not enable it, it takes the plugin down. -
The matcher narrows one level too shallow.
dispatchMatchingTriggersfilters on(organizationId, kind, enabled, ownerUserId). Its own comment explains whyownerUserIdexists — “a Google connection is one person’s mailbox” — and that argument repeats one level down: with two mailboxes, both fire every trigger the owner has. - Triggers have nowhere to say which account. No trigger config carries a connection, and the editor never asks.
Settled
- The shape of the need: work and personal on one connector, same person.
- Selection is per call, not a single default the user switches.
- An account is visible in the tool surface, so the agent cannot be vague about which one it acted as.
- The automation ask is about event routing first. Tool access inside a run comes later.
- The prompt is asked whenever the owner has 2+ connections for a connector — not only when the prompt mentions that plugin — once per trigger.
- Connecting a second account pins existing triggers to the first. An unrelated action must not change what a running automation reacts to; the new account fires nothing until someone asks it to.
Open: where the pin lives
Recommended
A column on automation_triggers
connection_id, narrowed in SQL beside the existing
ownerUserId clause. A mis-match leaks a personal inbox
into a work automation’s run — the same class of boundary
that clause already guards in SQL rather than in JS. An FK makes a
disconnect detectable instead of dangling.
Cost: a migration, and an explicit disconnect path, so a disconnect cannot silently widen a trigger back to “any”.
Inside the trigger config
Consistent with how scopes already work: the matcher already loads
every (org, kind) candidate and filters in JS, and the
editor and save path carry config for free. No migration.
Cost: no referential integrity, and the narrowing sits in the same JS
as {mode:"me"} resolution — a config-shape bug or an
unmigrated row fails open.
On the automation, not the trigger
One picker in the settings card, and it hands you the tool-access pin later for free.
Cost: an automation with a Linear trigger and a Slack trigger needs two values anyway, so it becomes a map — and the ask reads as per trigger.
Open: how per-account tools appear
Per-connection entries in the agent config
Literally gmail-work and gmail-personal in
the agent’s MCP config.
Cost: desiredMcpServers() is built from the static catalog
with no network, at boot. Per-connection entries make plugin
provisioning depend on a live API call, and every connect or disconnect
has to re-sync every machine.
Recommended
One entry, per-account tools from the proxy
The config stays gmail → bare URL, exactly as now.
The proxy, seeing two live connections, advertises the tool set once
per account under an account-qualified name and resolves the connection
itself.
Same thing the agent sees — the account is in the tool name — with no config churn and nothing new to provision offline.
Not yet decided
Tool access during an automation run is deliberately out of scope for now: whether a run sees only its pinned account, or all of them with the choice stated in context. Worth settling before the tool surface ships, because a scheduled run is the case where acting as the wrong account is least likely to be noticed.