Sales sequences, as code.
Own your sales sequencing infrastructure without building it from scratch.
Save $2,000+ a month on Outreach, Salesloft, Apollo, lemlist and Customer.io.
List prices for a 15-rep team: Outreach and Salesloft run $100 to $175 a seat each month, Apollo and lemlist $79 to $119 a seat plus credits, and Customer.io Premium starts at $1,000 a month. OpenSequence runs on one container and a Postgres.
Build with agents
MCPllms.txt01Use cases
Every use case, in one TypeScript file
import { OpenSequence } from '@opensequence/sdk';
const { SEQ_URL, SEQ_KEY } = process.env;
const oseq = new OpenSequence({ baseUrl: SEQ_URL!, apiKey: SEQ_KEY! });
const email = {
subject: 'Hi {{ contact.first_name }}',
body: 'Quick question…',
};
const { sequence, version } = await oseq.sequences.create({
name: 'UK intro',
definition: {
entry: 'hi',
exit_on: ['reply'],
nodes: {
hi: { type: 'outreach', channel: 'email', action: 'send', params: email },
},
},
});
await oseq.sequences.versions.publish(sequence.id, version.id);A sequence is a JSON document. Create it with one call.
Read the guideimport { OpenSequence } from '@opensequence/sdk';
const { SEQ_URL, SEQ_KEY } = process.env;
const oseq = new OpenSequence({ baseUrl: SEQ_URL!, apiKey: SEQ_KEY! });
const { items } = await oseq.sequences.list();
const ukIntro = items.find((s) => s.name === 'UK intro')!;
await oseq.senders.upsert('max', { name: 'Max Rivera', email: 'max@acme.com' });
await oseq.contacts.upsert('dana', {
first_name: 'Dana',
email: 'dana@example.com',
});
// First email at 09:12 Dana's time, from Max's own Gmail
await oseq.sequences.enroll(ukIntro.id, { contacts: ['dana'], sender: 'max' });Enroll a contact with a sender. The engine schedules and sends every step after that.
Read the guideimport { OpenSequence } from '@opensequence/sdk';
const { SEQ_URL, SEQ_KEY } = process.env;
const oseq = new OpenSequence({ baseUrl: SEQ_URL!, apiKey: SEQ_KEY! });
const { items } = await oseq.sequences.list();
const ukIntro = items.find((s) => s.name === 'UK intro')!;
// A new draft, copied from the live version
const draft = await oseq.sequences.versions.create(ukIntro.id, { tag: 'v2' });
const { definition, lockVersion } = draft;
// After the first email is sent, follow up in the same thread
const followUp = { subject: 'Re: hi', body: '…', reply_in_thread: true };
definition.nodes.hi.next = { sent: 'bump' };
definition.nodes.bump = { type: 'outreach', channel: 'email', action: 'send' };
definition.nodes.bump.params = followUp;
await oseq.sequences.versions.saveDraft(ukIntro.id, draft.id, {
definition,
lock_version: lockVersion,
});
await oseq.sequences.versions.publish(ukIntro.id, draft.id);Copy the live version, add a step, publish. Running enrollments keep their version.
Read the guideimport { OpenSequence } from '@opensequence/sdk';
const { SEQ_URL, SEQ_KEY } = process.env;
const oseq = new OpenSequence({ baseUrl: SEQ_URL!, apiKey: SEQ_KEY! });
await oseq.senders.upsert('max', { name: 'Max Rivera', email: 'max@acme.com' });
await oseq.senders.setLinkedin('max', {
address: 'https://linkedin.com/in/max',
});
// Use it in any sequence: Max gets a task with the contact's profile to open
const linkedinStep = {
type: 'task',
params: { kind: 'linkedin_connect', title: 'Connect on LinkedIn', due: '1d' },
};Link a rep's LinkedIn. Each LinkedIn step becomes a task for that rep.
Read the guideimport { OpenSequence } from '@opensequence/sdk';
const { SEQ_URL, SEQ_KEY } = process.env;
const oseq = new OpenSequence({ baseUrl: SEQ_URL!, apiKey: SEQ_KEY! });
await oseq.senders.upsert('max', { name: 'Max Rivera', email: 'max@acme.com' });
// Max opens auth_url once: Gmail send, Gmail read, Calendar read.
// Tokens renew on their own; every email leaves from max@acme.com.
const { auth_url } = await oseq.senders.connectGoogle('max');Each rep connects Gmail once. Every email leaves from their inbox.
Read the guideimport express from 'express';
import { OpenSequence, verifyWebhook } from '@opensequence/sdk';
const { SEQ_URL, SEQ_KEY } = process.env;
const oseq = new OpenSequence({ baseUrl: SEQ_URL!, apiKey: SEQ_KEY! });
const { secret } = await oseq.webhooks.create({
url: 'https://crm.acme.com/hooks/seq',
event_types: ['enrollment.exited'],
});
const app = express();
app.post('/hooks/seq', express.raw({ type: '*/*' }), (req, res) => {
const event = verifyWebhook({ secret, headers: req.headers, body: req.body });
console.log(event.type, event.data.exit_reason); // exited, replied
res.sendStatus(204);
});Register an endpoint once. Exits, replies and tasks arrive signed.
Read the guideimport { OpenSequence } from '@opensequence/sdk';
const { SEQ_URL, SEQ_KEY } = process.env;
const oseq = new OpenSequence({ baseUrl: SEQ_URL!, apiKey: SEQ_KEY! });
const { items } = await oseq.sequences.list();
const ukIntro = items.find((s) => s.name === 'UK intro')!;
const stats = await oseq.analytics.summary(ukIntro.id);
// enrolled: 412, replies: 61, reply_rate_per_100_enrolled: 14.8
// Which step earns the replies
const { nodes } = await oseq.analytics.nodes(ukIntro.id);Analytics count each reply and meeting once, per sequence and per step.
Read the guide02Why own it
Why teams own their sequencing
Price is the obvious reason. Here are the others.
Headless by design
OpenSequence has no UI of its own. Reps run sequences from the CRM they already use.
Data in your Postgres
Contacts, messages and replies live in your own Postgres, in the region you choose. No vendor stores your prospect data.
Your deliverability
Mail leaves each rep's own Gmail, not a shared sending pool. Another company's bad list cannot hurt your domain's reputation.
Sequences as code
A sequence is versioned JSON. Diff it, review it in a pull request and test it against mock providers in CI.
Agent-native
The MCP server ships with the engine. An agent with an enroll key can enroll contacts and read journeys, but cannot edit sequences, senders or settings.
No black boxes
The engine writes every decision to a ledger, with the branch it took and the facts it read. One GET returns the whole journey.
Multi-tenant
One deployment holds a project per customer, each with its own keys. Add sequencing to your own SaaS without building the engine.
Extensible
Channels, node types and providers register by name. Add WhatsApp or your own dialer without forking the engine.
You can vibe-code a sequence. Not a sequencing service.
The first version takes a weekend. The next two years go to edge cases, and you find each one in production.
If you build it yourself
Day 1
A cron job and a loop send the first email. It works, and everyone moves on.
Week 3
A worker restarts mid-send. Forty prospects get the same email twice.
Month 2
The code matches replies by subject line, so people who answered still get the follow-up.
Month 6
Texts arrive at 11 pm in the contact's time zone. Opt-outs climb, and you are the one on call.
03Nine problems
What you would have to solve
04FAQ
Questions
What people ask before the quickstart.
Is this a free alternative to Apollo, Lemlist or Outreach?
What does headless mean here?
Can I configure everything through the API?
How do agents use it?
Does it send email itself?
What do I need to run it?
05Get started
Stop paying per seat to send sequences.
Start the image, create a project and follow the six calls in the quickstart. Mock providers are on in development, so nothing leaves your machine until you connect a real account.