Engine
A deterministic engine with a generative layer.
How Claros runs deterministic lifecycle email.
Design
Deterministic execution. Generative content.
The engine handles timing, targeting, state machines, frequency caps, and send windows. AI participates only when compiling a flow and when drafting a message. The result is a dual engine: deterministic where correctness matters, generative where language matters.
Compilation is one LLM call per prompt change. AI-drafted content is three calls per message per contact (decide, draft, assess). Once compiled, the plan runs entirely in SQL until the operator asks to recompile. AI never touches the send path.
Read the compilation architecture →Generative layer
Compile time: prompt → deterministic plan
Content time: decide, draft, assess
Execution engine
Timing, targeting, state machines
Frequency caps, send windows, suppression
pg-boss queue, advisory locks, pgvector
Architecture
One database does everything
Claros uses pg-boss for jobs, advisory locks for deduplication, table partitioning for events, and pgvector for knowledge-base search. There is no Redis, no Kafka, and no separate broker to fail.
Every operational concern is solved inside the database you already know how to run, back up, and restore. A complete backup is a single pg_dump plus two environment variables.
Correctness
Built to not send twice
Lifecycle email has real consequences: a contact who receives the same onboarding email twice does not know it was a race condition. Every correctness mechanism below exists because of a specific class of failure it prevents.
- Atomic compare-and-set state transitions on every message. Status can only move forward:
pending→approved→sending→sent. A concurrent worker that loses the race gets a conflict, not a duplicate send. - Advance-only feedback: sent → opened → clicked, never backward. A late-arriving open event on an already-clicked message does not regress the contact's engagement record.
- Duplicate-pending check before creating any message. Before the engine creates a new pending message, it checks for an existing pending message for the same contact and flow step.
- Partial unique indexes on active flow memberships. Races between concurrent enrollments cannot double-enroll a contact in the same flow. The database enforces this, not application code.
- Idempotent event ingestion. A unique index on
(tenant_id, message_id)with a rolling deduplication window means replaying events from your SDK does not create duplicate lifecycle signals. - Reap worker recovers stuck messages hourly. Messages that fail mid-flight are retried up to three times before being marked failed. Failed messages are preserved for audit, not silently dropped.
Timing
Timezone-aware, not timezone-naive
Per-step window policies let welcome emails send immediately while nurture emails wait for the contact's local weekday window. Critical flows bypass the send window; suppression never does.
Timezone policy is configurable per tenant: contact_local (uses the timezone from the contact's identify traits) or tenant_fixed (a single configured timezone for all contacts). Configurable send-window hours and days.
Drain runs every 15 minutes as backstop. Approvals and event-triggered enrollment send within seconds. Worst-case event-to-send is roughly 35 minutes if a job is lost and caught by the sweep.
Read about send windows and throttling →Throttle
Frequency caps that protect your contact list
Per-tenant configurable frequency caps prevent message fatigue and protect deliverability. Caps apply across all nurture flows for a contact; the contact cannot be sent more than the configured maximum even if multiple flows target them simultaneously.
Default limits are conservative by design: 1 email per user per day, 2 per week, with a minimum 48-hour interval between emails. All defaults are adjustable.
Scale
From one container to many
Run --role=all on a single server to start. When you need to scale, split into separate api, worker, and scheduler roles. Workers pull jobs via FOR UPDATE SKIP LOCKED, so any number of workers can safely compete for the same queue.
Exactly one scheduler must run at any time. Any number of API and worker replicas can run in parallel. The scheduler is stateless: it reads from the database and queues jobs; it holds no local state.
Read the deployment guide →Three commands on your own server.
Self-hosting is the only way to run Claros today. Install in about five minutes and send your first real email from your own server.
git clone https://github.com/claroshq/claros.git && cd claros
docker compose run --rm install
docker compose up -dNo LLM key needed to start.Full installation guide →