Developer ToolsFree generatorPDF, Excel, Word export

ERP Event Messaging Outbox Table SQL Generator

If an ERP posts a message directly to WhatsApp or Teams inside a transaction, a slow API call holds locks and a failed one loses the notification. A transactional outbox solves it: write the message to a table with the business change, and let a worker send it. This generator produces the SQL.

Fill in

Generated code
Download:

PostgreSQL: table

-- Generic names: adapt column names and keys to your own schema.
CREATE TABLE IF NOT EXISTS messaging_outbox (
  id            BIGSERIAL PRIMARY KEY,
  channel       VARCHAR(20)  NOT NULL,
  event_type    VARCHAR(60)  NOT NULL,
  dedupe_key    VARCHAR(120) NOT NULL,
  recipient     VARCHAR(120) NOT NULL,
  payload       JSONB        NOT NULL,
  status        VARCHAR(12)  NOT NULL DEFAULT 'pending',
  attempts      INT          NOT NULL DEFAULT 0,
  next_attempt  TIMESTAMPTZ  NOT NULL DEFAULT now(),
  provider_id   VARCHAR(120),
  last_error    TEXT,
  created_at    TIMESTAMPTZ  NOT NULL DEFAULT now(),
  sent_at       TIMESTAMPTZ,
  CONSTRAINT uq_messaging_outbox_dedupe UNIQUE (channel, dedupe_key)
);

CREATE INDEX IF NOT EXISTS ix_messaging_outbox_due
  ON messaging_outbox (next_attempt) WHERE status = 'pending';

PostgreSQL: enqueue in the same transaction as the business change

INSERT INTO messaging_outbox (channel, event_type, dedupe_key, recipient, payload)
VALUES ('whatsapp', 'shipment_dispatched', 'SO-104522-dispatch', '919800000000',
        '{"order":"SO-104522","eta":"2026-10-21"}')
ON CONFLICT (channel, dedupe_key) DO NOTHING;

PostgreSQL: worker claims a batch

WITH due AS (
  SELECT id FROM messaging_outbox
  WHERE status = 'pending' AND next_attempt <= now() AND attempts < 6
  ORDER BY next_attempt
  LIMIT 20
  FOR UPDATE SKIP LOCKED
)
UPDATE messaging_outbox o
SET status = 'sending', attempts = o.attempts + 1
FROM due
WHERE o.id = due.id
RETURNING o.id, o.channel, o.recipient, o.payload;

PostgreSQL: record the result

-- On success
UPDATE messaging_outbox SET status = 'sent', sent_at = now(), provider_id = 'wamid.XXXX' WHERE id = 1;

-- On transient failure: exponential backoff, back to pending
UPDATE messaging_outbox
SET status = 'pending', last_error = 'HTTP 429',
    next_attempt = now() + (power(2, attempts) * interval '30 seconds')
WHERE id = 1;

-- Give up after the maximum attempts
UPDATE messaging_outbox SET status = 'failed' WHERE status = 'pending' AND attempts >= 6;

SQL Server: worker claim equivalent

WITH due AS (
  SELECT TOP (20) *
  FROM messaging_outbox WITH (UPDLOCK, READPAST, ROWLOCK)
  WHERE status = 'pending' AND next_attempt <= SYSUTCDATETIME() AND attempts < 6
  ORDER BY next_attempt
)
UPDATE due
SET status = 'sending', attempts = attempts + 1
OUTPUT inserted.id, inserted.channel, inserted.recipient, inserted.payload;

Table and column names here are generic, not an ERP vendor schema. Insert the outbox row in the same database transaction as the business change, then let a separate worker send. Delivery is at least once, so rely on the dedupe key and provider message IDs to avoid duplicates. SQL Server needs a datetime2 type for next_attempt and nvarchar(max) for payload; adjust types. Reset rows stuck in sending after a timeout.

Outbox design review

Optional review of your ERP notification pipeline for reliability and duplicates.

Optional. The tool and every export work without this. No spam.

How to use the SQL

Run the table script in a database you control, not inside a vendor ERP schema unless your vendor supports custom tables. Have your integration or a trigger insert a row when the business event commits. Run one or more workers that claim a batch with the locking query, call the messaging API, and write the result. Choose the PostgreSQL or SQL Server variant that matches your platform.

  • The unique dedupe key stops the same event queuing twice.
  • SKIP LOCKED or READPAST lets several workers run without blocking.
  • Backoff pushes next_attempt out after each transient failure.
  • A failed status gives support a list of messages that need a human.

Why the outbox pattern

It separates the business commit from the network call. If the messaging API is down, the ERP transaction still succeeds and the queue drains later. If the worker crashes after sending but before updating, the message may be sent again, so delivery is at least once and the provider message ID or dedupe key is your protection.

Worked example

A shipment is posted at 16:05. The same transaction inserts an outbox row with the dedupe key SO-104522-dispatch. A worker claims it at 16:05:10, calls the WhatsApp template send, stores the returned message ID and marks it sent. If the call returns 429, the row returns to pending with next_attempt 60 seconds later and attempts incremented.

Common mistakes

Forgetting an index on due rows makes the polling query slow once the table grows. Never purging sent rows lets the table balloon, so archive or delete after a retention period. Leaving rows in sending forever after a worker crash silently drops messages.

Frequently Asked Questions

What is a transactional outbox?

It is a table where an application writes the messages it needs to send, in the same transaction as its business data. A separate process reads the table and delivers the messages, so a failure in delivery never rolls back or loses the business change.

Why use SKIP LOCKED or READPAST?

They let several workers pull rows from the same table without waiting on each other. Each worker skips rows another worker has locked, which gives parallel throughput without double sending the same row.

Does this give exactly once delivery?

Not strictly. It gives at least once delivery with strong duplicate protection from the dedupe key and stored provider IDs. True exactly once across an external API is not generally achievable.

Can I add this to a vendor ERP database?

Check your ERP vendor's support terms first. Many teams put the outbox in a separate integration database and replicate or call into it, to avoid upgrade and support conflicts with the vendor schema.

If you want a reliable outbox and worker layer between your ERP and messaging channels, Netray can design and run it with your DBA team.