Sales sequences, as code.
Build your own sales sequencing infrastructure without building it from scratch.
No per-seat pricing. No black boxes. Just infrastructure you control.
Use cases
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.
Learn moreimport { 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.
Learn moreimport { 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.
Learn moreimport { 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.
Learn moreimport { 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.
Learn moreimport 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.
Learn moreimport { 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.
Learn moreWhy 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
Nine problems you would have to solve
Hover or tap a card to see how OpenSequence solves itHave your agent build and run your sequences.
Your agent drafts sequences, enrolls people and reads the results, through the same API as your backend.
Build a sequence for UK facilities managers: an intro email, a threaded follow-up after 3 days, then a call task.
Thought for 3s
Three steps, and it should stop when someone replies or books. I will check the definition before writing it to the repo.
Published UK facilities (v1). It stops on a reply or a booked meeting.
Enroll the 40 leads from this morning with Max as the sender.
Thought for 2s
First make sure Max can send, then dry-run the enrollment before writing anything.
Enrolled 38. I skipped two people who are already in "Q4 renewals". First emails go out tomorrow from 09:00, in each lead's own time zone.
Connect in one line
- Claude Code
- Cursor
- Codex
- Any MCP client
Scoped by its key
An enroll key cannot touch senders or settings.
Hosted or local
Run one HTTP endpoint for the team, or stdio on a laptop.
Same rules
The engine refuses double enrollment for agents too, and logs every call.
Questions
What people ask before the quickstart.
Is this a free alternative to Apollo, Lemlist or Outreach?
What does headless mean here?
Is it really fully configurable through the API?
How do agents use it?
Does it send email itself?
What do I need to run it?
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.