Skip to content

Tag corporate vs self-serve at signup

At the moment a new account is created, infer the user's tier — enterprise or self_serve — from their email domain and write it to app_metadata so every flow and your application can branch on it without a back-channel lookup.

B2B products with both a self-serve trial and a direct sales motion need to know, at signup, which type of user just arrived. Cymmetri uses this pattern to show enterprise signups a white-glove onboarding path and notify the sales team immediately, while self-serve users get the standard activation flow. The segmentation happens automatically, with no coupon code, no manual tagging, and no post-signup enrichment job.

The tag written here is permanent in app_metadata until you overwrite it. Every future login can read it back as event.user.app_metadata.tier, and a companion action (inject tier into JWT) can embed it directly in the access token so your application never needs to look it up.

A custom action attached to the Registration flow's Post-Create stage. The action reads event.user.email, lowercases the domain, checks it against a map of known enterprise customer domains, writes tier and signup_segment to app_metadata, and publishes the segment to the step-state store so any downstream block in the same run can read it. Optionally, it also fires a Slack or CRM notification for enterprise signups.

The complete configuration is:

  1. A custom action named tag-corporate-vs-self-serve, trigger Registration.
  2. A Run Custom Action block added to the Post-Create stage of the Registration flow.
  3. The Run Custom Action block configured to run tag-corporate-vs-self-serve.

Open Custom Actions and click + New Action.

Set:

  • Name: tag-corporate-vs-self-serve
  • Trigger: Registration

Click Save.

Step 2 — Edit the domain list in the code

Section titled “Step 2 — Edit the domain list in the code”

Open the code section below and replace the example domain list with your actual enterprise customer domains. Add the destination host for any optional notification call to Tenant Settings > Custom Actions > Fetch Allowlist before testing.

Paste the code into the action editor and click Save. Toggle the action to Enabled.

Step 4 — Add the block to the Registration flow

Section titled “Step 4 — Add the block to the Registration flow”

Navigate to Flows → Registration → Edit. Expand the Post-Create stage.

Drag a Run Custom Action block into Post-Create. In the configuration panel, select tag-corporate-vs-self-serve. Click Save.

Post-Create is the right stage — event.user.id is stable and the identity is committed.

If you also have the welcome-new-users action in Post-Create, place tag-corporate-vs-self-serve first. The tagging action writes step.<slug>.tier into step state; a welcome action placed after it can read that value without re-parsing the email domain.

Click Publish. All new registrations from this point forward will be tagged.

async function tagCorporateVsSelfServe(event, api) {
// Map of email domains that belong to enterprise customers.
// Replace these with your actual customer domains.
// For a production deployment, fetch this map from a URL on your
// tenant's fetch allowlist rather than hard-coding it here.
const enterpriseDomains = {
'cymmetri.com': 'enterprise',
'acme-corp.com': 'enterprise',
};
const email = event.user.email ?? '';
const domain = email.split('@')[1]?.toLowerCase() ?? '';
// Determine the tier. Any domain not in the map defaults to self_serve.
const tier = enterpriseDomains[domain] ?? 'self_serve';
const signupSegment = tier === 'enterprise' ? 'b2b' : 'self_serve';
// 1. Persist the tier to the admin-controlled metadata bag.
// This value survives across all future logins and can be read back
// as event.user.app_metadata.tier in any later flow.
api.user.setAppMetadata({
tier,
signup_segment: signupSegment,
tier_tagged_at: new Date().toISOString(),
});
// 2. Publish the tier to step state so a downstream block in THIS run
// (e.g. welcome-new-users or inject-tier-into-jwt) can read it via
// step['tag-corporate-vs-self-serve'].tier without another lookup.
api.state.set('tier', tier);
api.state.set('signup_segment', signupSegment);
api.log('info', `Signup tagged: ${tier} / ${signupSegment} (domain: ${domain})`);
// 3. Optional: notify the sales team for enterprise signups.
// Add the Slack webhook host to Tenant Settings > Fetch Allowlist first.
// Remove this block entirely if you do not need sales notifications.
if (tier === 'enterprise') {
try {
await api.fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/PATH', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `New enterprise signup: ${email} (domain: ${domain})`,
}),
});
api.log('info', `Sales notification sent for ${domain}`);
} catch (err) {
// Log the failure but do not deny — a missed notification is
// not a reason to block a paying customer from registering.
api.log('warn', `Sales notification failed: ${String(err)}`);
}
}
}

Open the tag-corporate-vs-self-serve action in Custom Actions and click Test. Select the New registration preset.

Test enterprise path: In the Event JSON editor, set user.email to someone@cymmetri.com (or another domain in your map). Click Run. In the API calls tab, confirm:

  • api.user.setAppMetadata was called with tier: 'enterprise' and signup_segment: 'b2b'.
  • api.state.set was called twice — once for tier and once for signup_segment.
  • If you included the Slack block: api.fetch appears with your Slack webhook URL.

Test self-serve path: Change the email to someone@gmail.com and run again. Confirm api.user.setAppMetadata is called with tier: 'self_serve' and signup_segment: 'self_serve', and that api.fetch does NOT appear (no sales notification for self-serve signups).

In neither test should you see api.deny.

Register a new account using an email address whose domain is in your enterprise map.

In the admin console, navigate to Users and open the new user's profile. Under App Metadata, confirm that tier, signup_segment, and tier_tagged_at are present with the expected values.

Navigate to Flows → Registration → Recent Runs and open the most recent run. The action should appear in the Post-Create stage with status Completed.

If you included the Slack notification: check your Slack channel. The notification message should have arrived within a few seconds of the registration completing.

The domain map is hard-coded. For a handful of enterprise customers this is fine. Once you have more than a dozen domains, or when customers change their email providers, a hard-coded map becomes a maintenance burden. Replace the inline object with an api.fetch call that downloads the current map from your own backend or a CDN-hosted JSON file on your fetch allowlist.

Domain inference is an approximation. It works reliably for B2B customers where everyone uses a company email. It breaks for customers who allow personal emails, or for consultants and agencies whose gmail.com address is their real work identity. If you have something better — an invitation token, an SSO connection identifier, a field in the OAuth request — use that instead.

The tier tag is a shallow merge. api.user.setAppMetadata merges at the top level. If app_metadata already has a tier key from a previous action, this call overwrites it. That is the intended behaviour here — the tagging action owns the tier key. If another action also writes to app_metadata, make sure they use distinct top-level keys.

Notify outside the flow when possible. The Slack notification in this recipe runs synchronously. If the Slack API is slow or unavailable, the registration flow waits. For high-volume tenants, consider posting to an internal queue endpoint that fans out to Slack (and your CRM, and your onboarding tool) asynchronously. The error-handling block above already suppresses the deny so a slow notification does not block the user — but it still adds latency.