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.

Jobspg-boss
DedupAdvisory locks
EventsTable partitioning
Embeddingspgvector
ConcurrencyFOR UPDATE SKIP LOCKED
No RedisNo KafkaNo broker

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: pendingapprovedsendingsent. 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 →
immediateWelcome emails, dunning, transactional. Sends within seconds of trigger.
respect_windowNurture, re-engagement. Waits for configured weekday hours in the contact's local timezone.
critical classBypasses frequency caps and send windows. Suppression list still applies - no exceptions.

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.

Per-day limit1 email (default)
Per-week limit2 emails (default)
Min interval48 hours (default)
Drain tickEvery 15 minutes
Batch size10 per drain (default)
Critical flowsBypass all caps

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 →
schedulerexactly 1
workerany number
workerany number
apiany number
Postgres 16 + pgvector

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 -d

No LLM key needed to start.Full installation guide →