---
title: "Palumb vs building it yourself: most teams should build this themselves"
url: "https://palumb.com/compare/build-it-yourself"
description: "Most teams should build notifications themselves. What changes that is waiting, tenants, and the record of every attempt."
---

# Palumb vs building it yourself: most teams should build this themselves

Written in your own repository, rendered by your own template engine, delivered through your own provider. The delivery path is open source, and the service is operated in the European Union under EU law alone.

[Join the beta](/waitlist/) [Jump to the comparison](#comparison)

Free up to 500 runs a month, and the beta is that free plan, so there is nothing to expire.

## Notification orchestration you stay in control of

building it yourself: The competitor almost everyone actually picks, and for most teams the right answer. Here is where that stops being true.

Palumb executes the notification workflows you write, and takes nothing else. There is no builder to define them in, no template system to adopt and no sending pool to share: the workflow is a function in your repository, the message is whatever your own engine rendered, and it leaves on your own provider credentials. What Palumb runs is the part that is genuinely hard: resolving who should be told, executing steps durably across days and deploys, and keeping the record of every attempt.

Why teams pick Palumb

-   Workflows in your repository, not Palumb's
-   No lock-in: your template engine, your provider
-   Open source, and free to self-host
-   One EU jurisdiction, nothing to authorise

## What you end up building

Every line comes from building it yourself's own documentation, checked on 25 August 2026.

### Retrying is easy until the process dies mid-send

Backoff is the easy half. The hard half is idempotency when the process restarts halfway through: the same run, the same step, and no second email. Palumb replays against the record of what already went out.

### Three days is longer than anything you deploy

A three-day delay is not a sleep. It has to outlive the container it started in, the release that happened on Tuesday and the node that went away. That is a durable execution engine, and it is the part nobody budgets for.

### A reminder you cannot call back is a bug you ship

The reminder must not go out if the invoice was paid an hour ago, so the wait has to be interruptible, and something has to know which run to interrupt. In Palumb the wait ends on a named event, or on its timeout.

### A digest is a queue you are now responsible for

Twenty events, one email on Monday. Someone has to hold the pile, decide when it is full, flush it on a schedule and not lose it if the process dies while holding it.

### Preferences never stay a boolean

Subscribers, topics, and the preference someone will inevitably ask to change. It starts as a boolean column and becomes a small product of its own the first time a customer asks to mute one kind of message.

### Sending as your customers multiplies your secrets

Once tenants send under their own provider, the transport is built per call and the keys need encrypting, rotating and keeping out of logs. Palumb encrypts them per tenant and builds the transport on each send, never a shared pool.

### You cannot build the attempt log on the day you need it

Accepted, failed or bounced, with the provider's reason and the workflow it came from, needed the day support asks, not before. Palumb keeps it for 90 days on every plan, the free one included.

## Palumb runs the workflow, and touches nothing else

The parts it runs are the ones that are easy to promise and slow to get right: waits survive a deploy, retries do not hammer the provider, and every attempt leaves a record.

[The full list is here](/features/).

A workflow branching into the three ways time enters it, all three of them steps in its body: gathering events until Monday, a durable delay of three days, and waiting for a shipped signal.

### Gathering, waiting, or sleeping for days.

A run can gather what arrives **until an instant your code names**, stay alive through **hours or days** without losing its place across a crash or a deploy, or pause until **your app sends a signal** or the timeout fires.

### Any sending provider.

Send through the **channel a workflow needs** (**Email, SMS, Telegram, Slack or push**), each on your own sending provider, never pooled with anyone else's.

### Fan-out.

**Subscribe a recipient to a topic** once; every **trigger** on that topic reaches everyone still on it, each with their own run. No loop you write yourself.

### However you reach it.

Open the **dashboard** to look, call the **REST API** to build on it, or point your own **AI agent** at the MCP server. Same data, three ways in.

### Tried, tracked, never twice.

A failed send is retried **on a schedule that backs off**, a retry never duplicates one that already went out (it carries the same **Message-ID**), and **every attempt** ends up in the record: accepted, failed or bounced, with the reason.

Three events reach one POST /sendEvent: comment.added, task.assigned, deadline.soon. It collects them into a single digest document with three entries and one email.

### Digests.

**Each event becomes one item** in a digest that builds up over a window; when it closes, every item goes out in **one notification**, not one message per event.

Four template syntaxes stacked: a Handlebars tag, a React Email component, an MJML tag, and a plain template string.

### Any template engine.

**Keep the template engine you already use**: MJML, React Email, Handlebars, or a plain string. Palumb never renders; it only delivers what you hand it.

## Just write the workflow

A workflow is a function in your codebase. Our SDK gives it durable steps (a send, a wait on the clock, a pause until something happens in your product), and each one survives a crash and a deploy. There is nothing to define in a dashboard, and no copy of it on Palumb's side to drift from yours.

Three stacked boxes in one column, joined by a rail. Nothing travels along the rail. Instead the stretch of rail between two boxes fills, from the caller down to the one being called, holds while that one works, and then drains back the way it came. The fill is always the same grey, the ink of the Palumb mark: the line belongs to Palumb, and which end it starts from already says who placed the call.

At the top yourapp.com/backend, your own application, with the calls it makes into Palumb: trigger, for the orderPlaced workflow on topic.order-24, and days later sendEvent, for the order.shipped event. Each call is marked with a small square that fills in once Palumb has it, and appears only when it happens, so nothing is on screen before you make it, and the rail below it fills until Palumb holds the run, then drains back.

In the middle palumb itself, the durable engine, and it is the one box that is not an address because it is not something you run. It says one thing, running workflow, and it blinks, because a live run is the only thing in the figure that is happening rather than recorded. It appears only when the trigger arrives: before you call, there is nothing of yours in Palumb. Underneath it a row of small purple tokens grows, one for each step that has closed, each carrying that step's number, so by the end there are five and the row is the journal.

At the bottom yourapp.com/workflows/orderPlaced, your own workflow code, holding the program itself: the five steps numbered one to five: step.send for the confirmation, step.waitForEvent for the shipped signal with a thirty day limit, step.send for the tracking, step.delay of three days, and step.send for the feedback. Palumb calls down into it one step at a time, the rail filling down to that line and draining back. The moment a step is recorded its number leaves the line, replaced by a tick, and reappears as a token under Palumb: you wrote the step, we are the ones who remember it ran.

The steps are yours because they are written in your repository; what Palumb keeps is the place in the list. Through the thirty day wait and the three day delay the rail down to your code is grey and empty while the run stays alive in Palumb, which is why the run survives a wait in which your server does nothing at all.

order-placed.ts

```ts
workflow("orderPlaced", async ({ payload, step }) => {
  await step.send("confirmation", "email", () => confirmation(payload))

  const shipped = await step.waitForEvent("shipped", {
    event: "order.shipped",
    timeout: "30d",
  })
  if (!shipped) return

  await step.send("tracking", "email", () => tracking(shipped))

  await step.delay("cool-off", { seconds: 3 * 24 * 3600 })
  return step.send("feedback", "email", () => feedback(payload))
})
```

Worked examples, with the code

-   [How do you follow an order all the way to feedback?](/use-cases/order-updates/)
-   [How do you chase an unpaid invoice without chasing someone who paid?](/use-cases/unpaid-invoice/)
-   [How do you nudge a user who signed up and never came back?](/use-cases/activation-reminders/)
-   [How do you warn before a trial ends, and stop the moment they upgrade?](/use-cases/trial-ending/)
-   [How do you send a weekly summary on a fixed schedule?](/use-cases/weekly-summary/)
-   [How do you stop sending one email per comment?](/use-cases/comment-bursts/)

## Palumb vs building it yourself compared

Ownership, deployment, jurisdiction, pricing and what each of those means once you have signed.

|  | building it yourself | Palumb | What this means for you |
| --- | --- | --- | --- |
| Available | Tonight: it is already running | Private beta, no track record | One of these is running tonight and the other is a waitlist. |
| Cost | Nothing to buy, and maintenance forever | Free to 500 runs, then €19, €49, €149 | The bill is not the comparison. The comparison is whose Tuesday afternoon goes into the next requirement. |
| Retries and idempotency | Yours to build, and to get right twice | A step that already sent is never run again | Backoff takes an hour. Not double-sending after a mid-run restart takes considerably longer. |
| A wait that survives a deploy | A durable execution engine, or a cron and hope | A step, with a timeout | This is the requirement that turns twenty lines into a system nobody signed up to maintain. |
| The record of every attempt | A table you design, and later migrate | 90 days on every plan | Nobody builds this on day one, and everybody needs it the day support asks. |
| Per-tenant provider credentials | Encryption, rotation, and keeping them out of logs | Encrypted per tenant, transport built per send | Fine with one sender. A project of its own the day your customers bring their own. |
| Where the logic lives | In your codebase | In your codebase | Palumb runs the function you wrote and keeps no copy of it, so the logic never changes hands. |
| What you operate | Whatever you already run | Nothing, or three services if you self-host | Handing this over is not a loss of control if the code is still yours and the exit is a licence. |

## Frequently asked questions

### Should I build my own notification system?

Usually, yes. Keep it in-house if you send a handful of transactional emails and each one is a single message with no waiting in between; if nothing has to survive a deploy, so re-running a dead job is harmless; if there is one product, one sender and one set of credentials; if nobody has yet asked who was notified, when, and what the provider answered; or if you already run a durable engine and the notification part is genuinely thin. In all of those, a vendor makes the system bigger rather than better.

### When does building it yourself stop being the right answer?

When notifications start waiting. A reminder in three days, a nudge unless something happens first, a digest on Monday. Each of those needs state that outlives the process. Add tenants who bring their own sending credentials, someone asking for the record of every attempt, and logic that has outgrown the cron with branches, escalations and cancellations, and the twenty lines have become a small system with no owner.

### What is the hardest part of building notifications in-house?

Not the sending: that is an SMTP call. It is not sending twice. A run that restarts halfway through has to know which steps already happened, which means the record of what went out has to be written before the send and consulted after it. The rest is work. That one quietly ships a bug to a customer's inbox.

### I already use Temporal or Inngest. Do I need anything else?

You already have the hard half, and it is genuinely the hard half. What a durable engine does not do is the notification layer on top: subscribers and topics, resolving “everyone subscribed to invoices” into recipients, preferences, per-tenant provider configuration and the delivery record. Palumb is that layer, with the engine underneath, so if you have already built it, buying it is not obviously worth it.

### Doesn't using a vendor mean giving up control?

That is the real reason teams build it themselves, and it is a good one. The answer here is not that it costs less: it is that the control stays yours. The workflow is a function in your repository, in your language. Your template engine renders the message and Palumb never sees it. Your provider sends it, on your credentials. And the whole delivery path is AGPL-3.0, so you can run it yourself or go back to your cron. Both doors stay open, which is the only version of this argument that can be proved.

## The other comparisons

Six of them, each conceding first. If you are drawing up a shortlist, these are the rest of it, and [the index compares all six side by side](/compare/).

-   [Palumb vs Novu](/compare/novu/)
    
    The other self-hostable one, and the closest thing we have to a mirror. Bigger product, EU regions, and a company outside the Union.
    
-   [Palumb vs Knock](/compare/knock/)
    
    Richer workflow functions and an in-app feed. A New York company, and no self-hosting path at all.
    
-   [Palumb vs Courier](/compare/courier/)
    
    More channels and real certifications. The only vendor here who publishes whose infrastructure runs its EU region, which is worth saying out loud.
    
-   [Palumb vs SuprSend](/compare/suprsend/)
    
    Multi-tenancy done properly, which is our own gap. Also a Delaware company processing data in the United States.
    
-   [Palumb vs MagicBell](/compare/magicbell/)
    
    An in-app inbox first, with a workflow engine of four commands and none of them waiting for an event. A San Francisco company that counts a delivery per channel.
    

Everyone who joins gets in, and it costs nothing. Your invite goes out when the beta opens.

[Join the beta](/waitlist/)