Brandfine Docs
Concepts

Routing Rules

Fire actions — email, webhook, Zapier hook — when a contact-form submission matches conditions you define.

Routing rules turn the Submissions inbox into a workflow. When a submission lands, every enabled rule for your team is evaluated in priority order: matched rules fire actions (email a teammate, POST to a webhook, ping Zapier / Make / your CRM). No matches? The submission still goes to the inbox — rules add behavior, they don't gatekeep it.

Rules apply team-wide. One ruleset covers every workspace under the team. To constrain a rule to a single workspace, add a condition on workspace.slug (or workspace.id, workspace.domain) — see Conditions below.

Where to find them

CMS → top of the Submissions page → Routing rules button. The pane opens as a side-sheet over the submissions list. The button badge shows enabled / total at a glance.

You need to be a team Owner or Admin to create, edit, enable, disable, delete, or reorder rules. Members can view the list and run dry-run tests.

The rule structure

Every rule has four parts:

  1. Name + description — for your team's benefit. Shown in the rule list.
  2. Conditions (optional) — when should this fire? Empty = every submission.
  3. Actions (at least one) — what should happen? Email, webhook, or both.
  4. Advanced — priority, stop-on-match, rate limit.

Conditions

A condition checks a single field on the submission. Build complex matches by stacking conditions and picking all of (AND) or any of (OR) at the top.

Field paths

Conditions reference submission fields via dot-paths:

PathWhat it is
nameSubmitter's name
emailSubmitter's email
messageFree-text body
subjectOptional subject line
phoneOptional phone number
sourceWhere the form was posted from (often a page path)
metadata.<key>Any field your form chose to send — metadata.locale, metadata.utm_source, metadata.formId, etc.
workspace.slugWorkspace the submission came from
workspace.idWorkspace ID
workspace.domainWorkspace custom domain (if set)
workspace.nameWorkspace display name

Any other dot-path that resolves against the submission JSON also works — the editor's dropdown shows the common ones but accepts arbitrary paths.

Operators

OperatorBehaviorValue type
existsPath resolves to a non-null value
not_existsPath is missing or null
equals / not_equalsExact string matchstring
contains / not_containsSubstring matchstring
starts_with / ends_withPrefix / suffix matchstring
regexJavaScript regex matchstring (the pattern)
in / not_inEquals any value in a listarray of strings

String operators are case-insensitive by default. Toggle Case-sensitive match on a condition row to flip that.

not_* operators are designed to do the intuitive thing when the path is missing: email not_contains "spam" matches a submission with no email at all.

Examples

Route enterprise leads to sales:

When message contains "enterprise" → email sales@yourco.

Only fire for one workspace in a team:

When workspace.slug equals marketing-site AND email ends_with @bigcorp.com → POST to your CRM.

Catch GDPR-region forms for a specific locale:

When metadata.locale in ["fr", "de", "es"] → email gdpr-team@yourco.

Actions

Each rule has one or more actions. Multiple actions on one rule fire in the order they're listed.

Email action

Drops a notification email to one or more recipients with the submission details (name, email, subject, message, source, metadata, plus the names of the rule(s) that matched). Up to 20 recipients per email action — stack multiple email actions if you need more.

The email subject line includes the workspace name + matched rule name so it's scannable in an inbox; the body shows the matched rule names as violet pills so multi-rule fires are obvious.

Webhook action

POSTs a JSON envelope to a URL you provide. Drop-in compatible with:

  • Zapier "Catch Hook" — paste the trigger URL, done.
  • Make.com webhooks
  • Slack incoming webhooks (raw JSON; you'll want to use Slack's Block Kit format on your side if you want pretty messages)
  • n8n, Pipedream, any custom HTTP endpoint

Payload

POST <your-webhook-url>
Content-Type: application/json
User-Agent: BrandfineRoutingWebhook/1.0 (+https://brandfine.co)
X-Brandfine-Signature: sha256=<hex>   # only when a signing secret is set

{
  "event": "submission.matched",
  "workspace": {
    "id": "ws_2N4r",
    "slug": "marketing-site",
    "name": "Marketing Site",
    "domain": "yourco.com"
  },
  "matchedRules": ["Enterprise leads", "All submissions audit"],
  "submission": {
    "id": "sub_2N5dRkQp",
    "name": "Alex Liu",
    "email": "alex@bigcorp.com",
    "phone": "+1 555 0100",
    "subject": "Demo request",
    "message": "I'd like a demo.",
    "source": "/contact",
    "metadata": { "utm_source": "twitter" },
    "createdAt": "2026-06-01T12:34:56.789Z"
  }
}

HMAC signing

Set a Signing secret (16–256 chars) on the action and Brandfine adds an X-Brandfine-Signature: sha256=<hex> header — HMAC-SHA256 of the raw JSON body bytes, hex-encoded. Verify with constant-time comparison on your side. Same convention as GitHub, Stripe, Slack webhooks.

Node example:

import { createHmac, timingSafeEqual } from 'node:crypto'

function verify(rawBody: string, header: string | undefined, secret: string) {
  if (!header?.startsWith('sha256=')) return false
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
  const received = header.slice('sha256='.length)
  return (
    expected.length === received.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(received))
  )
}

Verify against the raw bytes you received — re-serializing the parsed JSON before HMAC will compute a different digest.

Reliability

Single attempt with a 10-second timeout, fire-and-forget. We log delivery failures but don't retry — the submission is already saved in your inbox, so a failed webhook doesn't lose data.

If you need at-least-once delivery, point the URL at Zapier (their infra retries) or your own queue.

Priority + stacking

Rules evaluate in priority order, lowest number first (default 100). By default, every matched rule fires — submissions can match many rules.

Stop on match changes that: if a stopOnMatch rule matches, no lower-priority rules are evaluated for that submission. Useful for "VIP routes": catch enterprise leads with a high-priority rule that has stopOnMatch on, so they don't also trigger the generic "all submissions" rule below.

Rate limit

Optionally cap how often a rule's actions fire: at most N fires per W seconds. The window is per-rule (not per-workspace, not per-recipient). When the cap is hit inside the window, the rule is treated as "did not match" for further submissions until the window rolls over — the submission still saves to the inbox.

Useful for:

  • Webhooks with strict daily quotas
  • Noisy forms during a spam wave
  • Capping email notifications during a campaign launch

Both fields (window + max) must be set together or neither.

Testing rules

Use Test rules at the bottom of the pane to dry-run a sample submission. Fill in the fields, optionally pick a workspace to simulate from (so workspace.* conditions resolve), hit Run. The result shows which rules would have matched in priority order — no emails fire, no webhooks fire.

What rules don't do

  • They don't replace your inbox. Submissions are persisted whether they match any rule or not.
  • They don't filter. There's no "drop this submission" action. Use spam protection on the consumer-side form (reCAPTCHA, hCaptcha) if you need that.
  • They don't run on historical submissions. Rules fire at ingest time. Saving a new rule won't backfill notifications for yesterday's submissions.
  • They don't retry failed actions. A failed email or webhook is logged once and abandoned. The submission row is unaffected.

REST API

Full reference: /docs/api/routing-rules.

On this page