Ship a Slack alert on new signup end-to-end
A focused walkthrough for adding a real integration to an app you already have. Pick any Appbricx project with end-user signup, connect Slack, write one CDC-triggered workflow, then simulate the trigger without waiting for a real user to sign up.
0. Assumptions
- You already have a project generated by Appbricx with end-user auth (Auth & RLS). Any of the lead intake or multi-user todo walkthroughs give you one.
- Your app has a
users(or equivalent) table into which a signup writes a row. Adjust the table name below if yours differs — the pattern is the same.
1. Connect Slack in one click
- Open the project → Integrations.
- Find Slack, click Connect, and finish the OAuth handshake to your workspace.
- Pick the default channel the integration should post into (for example
#signups). Note the channel id — the workflow can override it per call, but a default keeps the code short.
Catalog + wire protocol details: Integrations.
2. Add a CDC binding
Fire a workflow every time a row is inserted into users. Edit (or create) .appbricx/backend/cdc/bindings.json:
{
"bindings": [
{
"id": "users-insert-slack",
"table": "users",
"ops": ["insert"],
"topic": null,
"workflow": "notify-slack-on-signup"
}
]
}Prefer runtime.upsert_cdc_binding if you have an editor script — DB + file stay aligned. Background on CDC: Topics & CDC.
3. Write the workflow
Create .appbricx/backend/workflows/notify-slack-on-signup.workflow.js:
/** @param {import("@appbricx/runtime").WorkflowContext} ctx */
export async function run(ctx) {
const row = ctx.trigger.payload?.row;
if (!row?.email) return { ok: true, skipped: true };
// Optional enrichment — total signups so far, for a nicer message.
const totalR = await ctx.queries.run("count_users");
const total = totalR.rows?.[0]?.count ?? null;
const text = total != null
? `New signup: ${row.email} (${row.name ?? "no name"}) — #${total} overall`
: `New signup: ${row.email} (${row.name ?? "no name"})`;
// ctx.integrations.invoke — provider + action; connection is resolved
// from the workspace integration set up in step 1.
const res = await ctx.integrations.invoke("slack", "chat.postMessage", {
channel: "#signups",
text,
blocks: [
{ type: "section", text: { type: "mrkdwn", text: `*${text}*` } },
{ type: "context", elements: [
{ type: "mrkdwn", text: `user_id: \`${row.id}\`` },
]},
],
});
if (!res.ok) throw new Error(res.error ?? "slack post failed");
ctx.log.info(`posted signup notice for ${row.email}`);
return { ok: true, ts: res.ts };
}Two things worth pointing out:
ctx.integrations.invoke(provider, action, args)is the universal shape for every catalog integration. Slack, Resend, Gmail, Notion, Linear — they all follow that call signature. See the ctx cheat sheet.- The workflow throws when Slack returns
ok: false. That marks the run as failed inapp_runtime_runs, so you can spot post-failures in Backend → Runs without reading logs.
4. Simulate a signup — do not wait for a real user
Two ways to fire the workflow before shipping to production users.
a) Invoke the workflow directly
Fastest — same code path as CDC, without needing to insert a row:
# SDK
await runtime.workflows.invoke("notify-slack-on-signup", {
payload: {
row: { id: "u_demo", email: "demo@example.com", name: "Demo User" },
},
});
# HTTP
curl -sS -X POST "$ORIGIN/__appbricx/runtime/workflows/notify-slack-on-signup/run" \
-H "Authorization: Bearer $DATA_TOKEN" \
-H "x-appbricx-data-api: 1" \
-H "content-type: application/json" \
-d '{"payload":{"row":{"id":"u_demo","email":"demo@example.com","name":"Demo"}}}'
# → { "ok": true, "runId": "…" }b) Insert a real row and let CDC fire it
Better for end-to-end confidence — exercises the CDC binding you added in step 2.
curl -sS -X POST "$ORIGIN/__appbricx/api/v1/users" \
-H "Authorization: Bearer $DATA_TOKEN" \
-H "x-appbricx-data-api: 1" \
-H "content-type: application/json" \
-d '{"email":"cdc@example.com","name":"CDC Test"}'Both variants land as a row in Backend → Runs. Auto CRUD reference: Auto CRUD REST API.
5. Inspect the run + logs
curl -sS "$ORIGIN/__appbricx/runtime/runs/$RUN_ID" \
-H "Authorization: Bearer $DATA_TOKEN" \
-H "x-appbricx-data-api: 1"
# Response shape (trimmed):
# {
# "run": {
# "id": "run_...", "workflow_id": "notify-slack-on-signup",
# "trigger_type": "cdc", "status": "succeeded",
# "started_at": "...", "finished_at": "...",
# "result": { "ok": true, "ts": "1712345678.001200" }
# },
# "logs": [
# { "level": "info", "msg": "posted signup notice for demo@example.com" }
# ]
# }Everything you see here is also available in the editor via Backend → Runs — same shape, easier to browse.
6. Optional — wrap the workflow in a webhook for external callers
Handy if a non-Appbricx system (Zapier, another SaaS, a cron on a laptop) also needs to trigger the alert. Add a webhook manifest and point it at the same workflow:
// .appbricx/backend/webhooks/notify-signup.json
{
"id": "notify-signup",
"name": "notify-signup",
"workflow": "notify-slack-on-signup",
"secret_ref": "WEBHOOK_SECRET",
"enabled": true
}Put the secret value into project env / vault under WEBHOOK_SECRET. See Secrets & env.
curl -sS -X POST "$ORIGIN/hooks/$PROJECT_ID/notify-signup" \
-H "content-type: application/json" \
-H "x-appbricx-webhook-secret: $WEBHOOK_SECRET" \
-d '{"row":{"id":"u_ext","email":"ext@example.com","name":"External"}}'
# → { "ok": true, "runId": "…" }Public route mechanics + rate limits: Webhooks & schedules.
Internal map (what just happened)
INSERT INTO users(...)
→ CDC emit (insert on 'users')
→ binding "users-insert-slack" enqueues workflow run
→ runner loads notify-slack-on-signup.workflow.js
→ ctx.queries.run("count_users") (optional enrichment)
→ ctx.integrations.invoke("slack", "chat.postMessage", {...})
→ Slack Web API (auth from workspace connection)
→ run status + logs in app_runtime_runsctx.integrations.invoke: one call signature, one hundred destinations.