Tell people and other systems
When something happens in an app, it can say so: a mail to a person, or a call to another system. You write what should happen in smithable.yml. The app keeps each message in an outbox, sends it after the change is saved, and tries again when it can't get through.
1. Mail a person
outgoing:
bookingMade:
when: Booking created # created | changed | deleted, or an action's name
to: mail
recipient: owner # the record's owner, a role (admin), or an email field
whenis<Model> created,changedordeleted, or the name of a button's action (it fires once the action has run).where(optional) narrows it:where: { status: shipped }. A change counts only when the record fits afterwards and did not fit before, so editing an order that is already shipped sends nothing.recipientcan be:owner;- a role (every user who has it);
- an
emailfield of the record, or one a reference away (customer.email); - a field that refers to a user.
- The mail has the subject and text from
smithable.content.yml(outgoing: { bookingMade: { subject: Your booking, text: … } }), then the record's fields as its page shows them, then a link to the record. The person who made the change is not mailed about their own change.
From the command line: smithable op addOutgoing bookingMade "Booking created" --to mail --recipient owner.
2. Call another system
outgoing:
incidentResolved:
when: Incident changed
where: { status: resolved }
to: statusFeed # a name; its address and secret are settings
The app posts JSON to the address set for statusFeed:
{ "event": "incidentResolved", "model": "Incident", "happened": "changed",
"at": "2026-10-13T08:00:00.000Z", "record": { "id": 12, "title": "…", "status": "resolved" } }
The record is as the API shows it. Each call carries three headers:
X-Event: the entry's name.X-Signature-256:sha256=and the HMAC-SHA256 of the body with the target's secret, so the receiver can check the call came from the app.Idempotency-Key: the same on every try of one call, so a receiver that got it once can tell a repeat.
A receiver checks the signature like this:
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
const ok = expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
Settings. A target's address and secret are two settings named after it: OUTGOING_STATUSFEED_URL and OUTGOING_STATUSFEED_SECRET.
- On your own server, they are environment variables.
- On smithable.ai, you set them on the project's Connections page.
- Until they are set, the calls wait in the outbox, and
smithable deploywarns about the target.
Which addresses the app will call. Only http or https. Never the app itself, and in a published app never a private or local network. The address checked is the one connected to. On smithable.ai, the app reaches the internet only through the service's egress proxy, and only for the Connections you added.
3. Delivery and its history
- The outbox: a mail or call is written in the same step as the change, and sent right after. So a change never waits for another system, and nothing is sent for a change that didn't happen.
- Retries: a failed one is tried again after 1, 2, 4… minutes, at most three hours apart, for a day. Then it is given up and shown.
- Activity (Team → Activity, for administrators) has a line for every mail and call. A call's line says when it was sent, which entry it was and the answer's status.
- A target's delivery history is Activity filtered to it: choose it under Sent to, or open
/. The page then starts with "Calls to statusFeed" and the last call's time and answer, so you see at once whether deliveries work.team/ activity?target=statusFeed - Send again: a mail or call that was given up has a Send again button.
- A target's delivery history is Activity filtered to it: choose it under Sent to, or open
- On smithable.ai, a call the service refuses says why in its line. An address you haven't connected yet waits until you add it on the Connections page.
4. In the preview
The preview sends no mail and makes no calls. Mails stay in its outbox, and calls to a target are skipped. Activity shows both as what a published app would do.