@aglyn/plugins-outreach
Sequences: one-to-one, multi-step email sequences a rep sends from their own connected mailbox, logged on the CRM's records. A sequence enrolls a contact or a lead (AGL-3234): an enrollment names which record the person is, a lead is judged and merged as the contact it would be, and when the lead converts its enrollment follows it to the contact through the core's lead-conversion seam.
Published to npm from the Aglyn monorepo. Versions follow semantic versioning.
Install
npm install @aglyn/plugins-outreach
A plugin is loaded through Aglyn's plugin manager (@aglyn/aglyn) by the console and the tenant runtime; it is not a standalone library.
The name people read is Sequences. The lib, the package, the
release flag, the entitlement, the permission key, the collections and the API
prefix stay outreach: they are stored vocabulary and live URLs, and no
reader sees them.
Two halves, like the CRM plugin:
- Console (
registerOutreachConsole, client barrel): the organization-level Sequences hub at/[orgSlug]/outreach/<section>, declared throughConsoleExtension.orgNavItems, behindrelease_outreach, thefeatures.outreachentitlement and theoutreach.usepermission. Behind the same gates, one widget in a zone the CRM hosts: Reply in thread from the mailbox in a contact's or a lead's header, before Send email (crmRecordEmailActions, AGL-4020). - Console API (
registerOutreachConsoleApi,@aglyn/plugins-outreach/server): handlers under theoutreach/prefix, served by the console's/api/[...pluginApi]dispatcher.
Two more entries, neither of them loaded by the tenant runtime:
- Console-only server declarations (
registerOutreachConsoleServerDeclarations,@aglyn/plugins-outreach/declarations.console-server): the workspace and account erasers, registered at the console's boot. They revoke a rep's Google grant withOUTREACH_TOKEN_KEY, which only the console holds. - Subprocessors (
outreachSubprocessors,@aglyn/plugins-outreach/subprocessors): the third-party hosts the plugin's code names, which the manifest generator writes into the console's subprocessor inventory.
The document model shared by both halves is src/lib/model/outreach.types.ts.
Every outreach* collection is written by the server alone.
Mailboxes
src/lib/mailboxes and src/lib/transport connect a rep's own
Google or Microsoft 365 mailbox and talk to it:
| module | does |
|---|---|
mailboxes/mailbox-routes | connect (signed single-use state, PKCE, OpenID nonce), settings, pause, test and disconnect |
mailboxes/oauth-state | the signed state and its pending record under orgs/{orgId}/outreachOAuthStates |
mailboxes/mailbox-credentials | the refresh token sealed with the shared secret box, in outreachMailboxCredentials |
mailboxes/mailbox-transport | opens a mailbox's client for its provider, and marks one reconnect-required |
mailboxes/mailbox-erasure | what a workspace or account erasure revokes and deletes |
transport/mail-client | the provider seam the runtime reaches a mailbox through, and the structured search |
transport/gmail-client | the fetch-based Gmail API client: send, full and metadata thread reads, search, send-as |
transport/graph-client | the Microsoft Graph client: a step sent as a draft from its own MIME, conversation and message reads, the windowed search |
transport/microsoft-oauth | Microsoft's identity platform: consent address, code exchange, refresh, ID token |
transport/mime-message | a raw MIME message read into the Gmail API shape the classifier takes |
transport/send-message | the one door a send goes through, including the engine's composed email |
transport/rfc5322 | the plain-text RFC 5322 writer |
OUTREACH_TOKEN_KEY, GOOGLE_OUTREACH_CLIENT_ID,
GOOGLE_OUTREACH_CLIENT_SECRET, MICROSOFT_OUTREACH_CLIENT_ID,
MICROSOFT_OUTREACH_CLIENT_SECRET and MICROSOFT_OUTREACH_TENANT are read by
mailboxes/outreach-config alone,
and outreach-credential-isolation.spec.ts holds that no tenant file reaches it.
The engine
src/lib/engine is every decision the sending runtime makes, as
pure functions with no I/O, exported from the package root:
| module | decides |
|---|---|
sequence-validation | what a sequence may be: 8 steps, 4 emails, 1–30 business days, subjects, bodies, windows, countries; and what activation refuses on |
gates | who may be emailed, over a contact as stored and the results of the runtime's lookups, with a sentence per block |
schedule | when a step comes due: business days in the mailbox's zone, clamped into the window, with jitter |
sending-capacity | the daily cap and warm-up ramp, the per-run allowance, and which due enrollments go first |
enrollment-state | the enrollment state machine, the new enrollment document, and what a completed step writes |
compose | the email a step sends: merge fields, the mandatory footer, threading, unsubscribe headers |
thread-message | the classifier's message shape, and the adapter from a Gmail API message |
delivery-status, opt-out-intent, thread-classification | what came back: bounces, automatic replies, replies and opt-outs |
mailbox-health | when a mailbox pauses itself |
mailbox-notice | the email a mailbox's owner gets when it pauses itself or needs reconnecting |
gateway-block | whether a hard bounce is the recipient domain's gateway refusing the sender, rather than one unknown address |
do-not-contact-domain | how a domain on the do-not-contact list is spelled |
manual-reply | answering a person who wrote back (AGL-4020): what the enrollment keeps of their messages, the in-thread answer (Re:, In-Reply-To, References, their message quoted) and what still refuses it |
engine/do-not-contact derives the do-not-contact document id with
node:crypto, so it is not in the barrel: import
@aglyn/plugins-outreach/engine/do-not-contact from server code.
The sending runtime
src/lib/runtime runs the engine against real mailboxes, in the
console alone — each run opens a rep's sealed grant:
| module | does |
|---|---|
runtime/send-job | outreach-send: due enrollments claimed in a transaction that reserves the mailbox's day, gates asked again, the email composed — with the List-Unsubscribe header when the sequence turns it on (AGL-3296), and its links' outreachLinks documents written first when it tracks clicks (AGL-3297) — and sent, the step recorded and filed on the contact's timeline; task steps file the rep's task |
runtime/sync-job | outreach-sync: replies, out-of-office answers, opt-outs and bounces read with format=full and classified by the engine; what the person wrote kept on the enrollment (AGL-4020); the mailbox's health evidence and its automatic pause |
routes/reply-routes, components/reply-dialog | Reply in thread (AGL-4020): outreach/enrollments/thread reads what the person wrote — from the mailbox, for a reply synced before replies were kept — and outreach/enrollments/reply sends a member's answer from the enrollment's mailbox in the same thread, kept as a manual_reply step record and on the CRM timeline, counted on health.daily.<day>.manualReplies and never against the cap; on a sequence that tracks clicks its links go out as short links, each a click of the person's and never the sequence's (AGL-4022) |
runtime/unsubscribe-link, runtime/unsubscribe-route | the signed one-click link (GET and RFC 8058 POST /api/outreach/unsubscribe), a recipient link that outlives a paused rollout, and the +unsubscribe mailto |
runtime/click-link, runtime/click-route | click tracking: the short link every send carries (GET /api/outreach/l/<id>, resolved through the top-level outreachLinks document, AGL-3297) and the signed GET /api/outreach/click?t=… link older emails carry; both recipient links, redirect first, then the click counted once per person with scanners kept apart; on the mailbox domain's verified links. host as https://links.<domain>/<id> (AGL-3306), which the console middleware rewrites to the same route |
routes/link-domain-routes, components/link-domains-card | Sequences → Mailboxes → Link domains (AGL-3306): outreach/link-domains lists each mailbox domain's links. host and sets one up, checks it or removes it through @aglyn/tenant-data-admin/server/tracking-hosts — the domain driver attaches it, an HTTPS probe verifies it; set-up and removal are an owner's or admin's |
runtime/enrollment-events | status changes as the engine's events, and an opt-out recorded on every list |
runtime/mailbox-notices | the owner told, once, when a mailbox pauses itself or needs reconnecting |
runtime/timeline | the contact's timeline, through the core's record-timeline seam |
runtime/person-erasure | the enrollments a person erasure deletes, and the do-not-contact entry it keeps |
Both jobs are declared on the core's plugin-console-crons from the
console-only declarations, which run them every fifteen minutes from
/api/admin/plugin-crons and give each a row on /api/health/crons. Their
dependencies are runtime/runtime-deps; runtime/fixtures/fake-gmail is the
in-memory mailbox the emulator specs run them against.
License
Apache-2.0. Source: https://github.com/aglyn/aglyn/tree/main/libs/plugins/outreach