Auth0 Actions: User Metadata Not Persisting Across Multi-Step Post-Login Flows
Overview
We are implementing a multi-step post-login wizard using Auth0 Actions and custom forms. The goal is to collect and validate user profile data (email, username, names, phone) in a sequence of steps, persisting the data between each step and ultimately issuing a JWT with a complete user profile.
The data is not persisting correctly across the steps, leading to incomplete user profiles in the final JWT. This document outlines the current implementation, the problem we are facing, and the steps we have taken to resolve it.
The implementation consists of five Actions, each responsible for a specific step in the user profile collection process. The Actions are triggered sequentially after the user logs in, and they interact with external APIs to validate the data.
The actions currently contain a lot of logging to help debug the issue, and we are using api.user.setUserMetadata()
to store the collected data in user_metadata
after each step which will obviously be removed.
The actions use the universal form rendering API to display forms to the user, and they handle the form submissions to validate and store the data.
Request for Help
- Is there a supported way to persist data between steps in a multi-step Auth0 Action flow, given that
user_metadata
is not immediately available in the same login session? - Is there a way to add hidden or read-only fields to Auth0 Action forms to carry data between steps?
- What is the recommended pattern for multi-step data collection in Auth0 Actions, given these limitations?
Any guidance or workarounds would be appreciated as would any best practices for structuring these flows given what we are trying to do.
The 5 Flows
- post_login_wiz1_checkEmail.js
- Validates the user’s email address via an external API.
- Sets the email in
user_metadata
if valid.
- post_login_wiz2_checkUserName.js
- Collects and validates
username
,given_name
, andfamily_name
via a form and external API. - Sets these fields in
user_metadata
.
- post_login_wiz3_checkPhone.js
- Collects and validates the user’s phone number via a form and external API.
- Sets
phone_number
inuser_metadata
.
- post_login_wiz4_checkRouting.js
- Determines which step is next based on missing fields in
user_metadata
. - Routes the user to the appropriate form.
- post_login_wiz5_markComplete.js
- Marks the profile as complete in
user_metadata
and finalizes the flow.
What We Are Trying to Do
- Guide the user through a series of forms to collect all required profile data.
- Validate each field using external APIs such as unique username within our system, unique phone etc.
- Persist all collected data between steps using
user_metadata
. - Ensure the final JWT contains all required fields.
The Problem
Observed Behavior:
- After each step, we use
api.user.setUserMetadata()
to store the collected data. - The logs show that the correct data is being set at each step.
- However, when the next step runs, the previously set data is missing from
event.user.user_metadata
. - This causes subsequent forms to re-request data already entered, and the final JWT is incomplete.
Example Log Excerpt:
12:51:29: [buildAndSetUserMetadata] Setting user_metadata: {"username":"vvvvvuser","given_name":"vvvfirst","family_name":"vvvvlast"}
12:51:29: [post_login_wiz3_checkPhone] user_metadata at entry: object {}
12:51:29: [post_login_wiz3_checkPhone] Missing phone_number, rendering phone form
- The username and names were set, but are missing when the phone step starts.
What We’ve Tried
- Merging all available data from the form and event in each handler.
- Rearranging the order of steps to ensure data is set before it is needed.
- Using
api.user.setUserMetadata()
to set data at each step, but it seems not to persist correctly across steps.
The flows
Here is the code for the post_login_wiz1_checkEmail.js
Action, which validates the email and sets it in user_metadata
:
const axios = require("axios");
// Standardized helper for building and setting user metadata
function buildAndSetUserMetadata(api, event, formFields = {}) {
const meta = {
username: formFields.username || event.user.username || event.user.user_metadata?.username,
given_name: formFields.given_name || event.user.given_name || event.user.user_metadata?.given_name,
family_name: formFields.family_name || event.user.family_name || event.user.user_metadata?.family_name,
phone_number: formFields.phone_number || event.user.phone_number || event.user.user_metadata?.phone_number,
email: formFields.email || event.user.email || event.user.user_metadata?.email
};
const filtered = {};
for (const [key, value] of Object.entries(meta)) {
if (value !== undefined && value !== null && value !== '' && value !== 'undefined' && value !== 'null') {
filtered[key] = value;
}
}
console.log('[buildAndSetUserMetadata] Setting user_metadata:', JSON.stringify(filtered));
api.user.setUserMetadata(filtered);
}
async function callValidationApi(secrets, endpointSecretKey, payload) {
const { ENVIRONMENT, API_HOSTNAME_TEMPLATE } = secrets;
const endpointPathAndKey = secrets[endpointSecretKey];
if (!ENVIRONMENT || !API_HOSTNAME_TEMPLATE || !endpointPathAndKey) {
throw new Error(`Configuration error: A required secret is missing for ${endpointSecretKey}.`);
}
// Construct the full URL from the secrets
const baseUrl = API_HOSTNAME_TEMPLATE.replace('${environment}', ENVIRONMENT);
console.log(baseUrl);
console.log(ENVIRONMENT);
const fullUrl = `https://${baseUrl}${endpointPathAndKey}`;
console.log(`Calling validation endpoint: ${fullUrl}`);
try {
const response = await axios.post(fullUrl, payload);
return response.data;
} catch (error) {
console.error(`API call to ${fullUrl} failed: ${error}`);
throw new Error('An external API call failed.');
}
}
exports.onExecutePostLogin = async (event, api) => {
// Only care about social / passwordless and FIRST login
console.log('[post_login_wiz1_checkEmail] user_metadata at entry:', typeof event.user.user_metadata, JSON.stringify(event.user.user_metadata));
console.log(`[post_login_wiz1_checkEmail] event data follows`);
console.log(JSON.stringify(event, null, 2));
if (event.stats.logins_count !== 1) {
console.log(`[post_login_wiz1_checkEmail] Only 1 login so can ignore as only care about social / passwordless and FIRST login`);
return;
}
if (event.connection.strategy === 'auth0') {
console.log('[post_login_wiz1_checkEmail] is auth0 database so email already captured during signup');
// Optionally, set email in metadata for consistency
buildAndSetUserMetadata(api, event, { email: event.user.email });
return;
} // database
try {
const { email } = event.user;
const socialAccountUsed = event.connection.strategy === 'auth0' ? "" : event.connection.name;
const result = await callValidationApi(
event.secrets,
'VALIDATE_EMAIL_ENDPOINT',
{ EmailAddress: email, SocialAccountUsed: socialAccountUsed }
);
console.log(`[post_login_wiz1_checkEmail] validate_email result: ${JSON.stringify(result)}`);
if (result.code !== 'EV_AVAIL') {
const userMessage = result.userMessage;
return api.access.deny('email_in_use', userMessage);
} else {
console.log('[post_login_wiz1_checkEmail] Email available');
// Set email in metadata for consistency
buildAndSetUserMetadata(api, event, { email });
}
} catch (error) {
console.error(`[post_login_wiz1_checkEmail] Error in 'Validate Email Address' action: ${error}`);
return api.access.deny('validation_api_error', 'An unexpected error occurred. Please try again.');
}
};
Here is the code for the post_login_wiz2_checkUserName.js
Action, which collects and validates the username and names:
const axios = require('axios');
const isMissing = v => v == null || v === '' || v === 'undefined' || v === 'null';
const buildUrl = (secrets, endpointKey) => {
const { ENVIRONMENT, API_HOSTNAME_TEMPLATE } = secrets;
const pathAndKey = secrets[endpointKey];
if (isMissing(ENVIRONMENT) || isMissing(API_HOSTNAME_TEMPLATE) || isMissing(pathAndKey)) {
throw new Error(`Missing secrets for ${endpointKey}`);
}
const host = API_HOSTNAME_TEMPLATE.replace('${environment}', ENVIRONMENT);
return `https://${host}${pathAndKey}`;
};
const callValidationApi = async (secrets, endpointKey, payload) => {
const url = buildUrl(secrets, endpointKey);
console.log(`[post_login_wiz2_checkUserName] → POST ${url} with payload: ${JSON.stringify(payload)}`);
const { data } = await axios.post(url, payload);
return data;
};
const usernameFormId = 'ap_ABCDE';
const apiErrorFormID = 'ap_FGHIJ';
// The form fields as defined in signup_username.json
const requiredFields = [
{ key: 'username', error: 'Please choose a username.' },
{ key: 'given_name', error: 'Please enter your first name.' },
{ key: 'family_name', error: 'Please enter your last name.' }
];
const hasAllFields = meta =>
requiredFields.every(f => !isMissing(meta?.[f.key]));
const validateFields = form => {
for (const { key, error } of requiredFields) {
if (isMissing(form[key]) || isMissing(form[key].trim())) {
return { valid: false, key, error };
}
}
return { valid: true };
};
// Standardized helper for building and setting user metadata
function buildAndSetUserMetadata(api, event, formFields = {}) {
// Combine data from form fields and event.user
const meta = {
username: formFields.username || event.user.username,
given_name: formFields.given_name || event.user.given_name,
family_name: formFields.family_name || event.user.family_name,
phone_number: formFields.phone_number || event.user.phone_number
};
// Only include defined, non-empty fields
const filtered = {};
for (const [key, value] of Object.entries(meta)) {
if (value !== undefined && value !== null && value !== '' && value !== 'undefined' && value !== 'null') {
filtered[key] = value;
}
}
console.log('[buildAndSetUserMetadata] Setting user_metadata:', JSON.stringify(filtered));
api.user.setUserMetadata(filtered);
}
exports.onExecutePostLogin = async (event, api) => {
const userMeta = event.user.user_metadata || {};
console.log(`[post_login_wiz2_checkUserName] onExecutePostLogin: user_metadata = ${JSON.stringify(userMeta)}`);
// Only render form if required fields are missing
if (!hasAllFields(userMeta)) {
console.log('[post_login_wiz2_checkUserName] Missing required fields, rendering username form');
return api.prompt.render(usernameFormId);
}
console.log('[post_login_wiz2_checkUserName] All required fields present, skipping username form');
};
exports.onContinuePostLogin = async (event, api) => {
console.log('[post_login_wiz2_checkUserName] user_metadata at entry:', typeof event.user.user_metadata, JSON.stringify(event.user.user_metadata));
const form = event.prompt?.fields || {};
console.log(`[post_login_wiz2_checkUserName] onContinuePostLogin: form = ${JSON.stringify(form)}, user_metadata = ${JSON.stringify(event.user.user_metadata)}`);
console.log('[post_login_wiz2_checkUserName] onContinuePostLogin START', JSON.stringify(event.user.user_metadata));
// Validate required fields
const validation = validateFields(form);
if (!validation.valid) {
console.log(`[post_login_wiz2_checkUserName] Validation failed for ${validation.key}: ${validation.error}`);
return api.prompt.render(usernameFormId, { errors: { [validation.key]: validation.error } });
}
// Only call the API if the username is different from what's already in metadata
const userMeta = event.user.user_metadata || {};
const usernameChanged = form.username !== userMeta.username;
if (usernameChanged) {
try {
const res = await callValidationApi(
event.secrets,
'VALIDATE_USERNAME_ENDPOINT',
{ Username: form.username }
);
console.log(`[post_login_wiz2_checkUserName] Username validation API result: ${JSON.stringify(res)}`);
if (res.code !== 'UN_AVAIL') {
console.log(`[post_login_wiz2_checkUserName] Username not available: ${res.userMessage || 'Username already taken'}`);
return api.prompt.render(usernameFormId, {
errors: { username: res.userMessage || 'Username already taken' }
});
}
} catch (error) {
console.error(`[post_login_wiz2_checkUserName] Username validation API call failed: ${error}`);
return api.prompt.render(apiErrorFormID);
}
}
// Set all metadata fields at once, combining across steps
buildAndSetUserMetadata(api, event, {
username: form.username,
given_name: form.given_name,
family_name: form.family_name
});
console.log(`[post_login_wiz2_checkUserName] Username and names accepted and saved: ${JSON.stringify({
username: form.username,
given_name: form.given_name,
family_name: form.family_name
})}`);
// Do not render the form or return anything else; let the wizard proceed
};
'Here is the code for the post_login_wiz3_checkPhone.js
Action, which collects and validates the phone number:
const axios = require("axios");
const isMissing = v => !v || v === "undefined" || v === "null";
const phoneFormId = 'ap_LMNOP';
const apiErrorFormID = 'ap_FGHIJ';
// Standardized helper for building and setting user metadata
function buildAndSetUserMetadata(api, event, formFields = {}) {
const meta = {
username: formFields.username || event.user.username,
given_name: formFields.given_name || event.user.given_name,
family_name: formFields.family_name || event.user.family_name,
phone_number: formFields.phone_number || event.user.phone_number
};
const filtered = {};
for (const [key, value] of Object.entries(meta)) {
if (value !== undefined && value !== null && value !== '' && value !== 'undefined' && value !== 'null') {
filtered[key] = value;
}
}
console.log('[buildAndSetUserMetadata] Setting user_metadata:', JSON.stringify(filtered));
api.user.setUserMetadata(filtered);
}
async function callValidationApi(secrets, endpointSecretKey, payload) {
const { ENVIRONMENT, API_HOSTNAME_TEMPLATE } = secrets;
const endpointPathAndKey = secrets[endpointSecretKey];
if (!ENVIRONMENT || !API_HOSTNAME_TEMPLATE || !endpointPathAndKey) {
throw new Error(`Configuration error: A required secret is missing for ${endpointSecretKey}.`);
}
const baseUrl = API_HOSTNAME_TEMPLATE.replace('${environment}', ENVIRONMENT);
const fullUrl = `https://${baseUrl}${endpointPathAndKey}`;
console.log(`Calling validation endpoint: ${fullUrl}`);
try {
const response = await axios.post(fullUrl, payload);
return response.data;
} catch (error) {
console.error(`API call to ${fullUrl} failed: ${error}`);
throw new Error('An external API call failed.');
}
}
exports.onExecutePostLogin = async (event, api) => {
console.log('[post_login_wiz3_checkPhone] user_metadata at entry:', typeof event.user.user_metadata, JSON.stringify(event.user.user_metadata));
const phoneNumber = event.user.phone_number || event.user.user_metadata?.phone_number;
if (!phoneNumber) {
console.log('[post_login_wiz3_checkPhone] Missing phone_number, rendering phone form');
return api.prompt.render(phoneFormId);
}
console.log('[post_login_wiz3_checkPhone] phone_number present, skipping phone form');
};
exports.onContinuePostLogin = async (event, api) => {
console.log('[post_login_wiz3_checkPhone] onContinuePostLogin called');
console.log('[post_login_wiz3_checkPhone] event.prompt:', JSON.stringify(event.prompt));
const form = event.prompt?.fields || {};
let phoneNumber = form.phone_number || form.telephone;
if (typeof phoneNumber === 'object' && phoneNumber.number) {
phoneNumber = phoneNumber.number;
}
if (!phoneNumber) {
console.log('[post_login_wiz3_checkPhone] No phone number found in form, re-rendering form with error');
return api.prompt.render(phoneFormId, { errors: { phone_number: 'Please enter your phone number.' } });
}
try {
const socialAccountUsed = event.connection.strategy === 'auth0' ? "" : event.connection.name;
const result = await callValidationApi(
event.secrets,
'VALIDATE_PHONE_ENDPOINT',
{ PhoneNumber: phoneNumber, EmailAddress: event.user.email, SocialAccountUsed: socialAccountUsed }
);
console.log(`[post_login_wiz3_checkPhone] VALIDATE_PHONE_ENDPOINT result:`, result);
if (result.code !== 'PN_AVAIL') {
console.log('[post_login_wiz3_checkPhone] Phone not available, re-rendering form with error');
return api.prompt.render(phoneFormId, {
errors: {
phone_number: result.userMessage || 'Phone number not available'
}
});
}
// Use the standardized helper to set all metadata fields, combining across steps
buildAndSetUserMetadata(api, event, { phone_number: phoneNumber });
console.log(`[post_login_wiz3_checkPhone] Phone validated and saved: ${phoneNumber}`);
} catch (error) {
console.error(`[post_login_wiz3_checkPhone] Error in 'Validate Phone' action: ${error}`);
return api.prompt.render(apiErrorFormID);
}
};
Here is the code for the post_login_wiz4_checkRouting.js
Action, which checks the user metadata and routes to the appropriate form:
const usernameFormId = 'ap_ABCDE';
const phoneFormId = 'ap_LMNOP';
const doneFormId = 'ap_QRSTU';
// Standardized helper for building and setting user metadata
function buildAndSetUserMetadata(api, event, formFields = {}) {
const meta = {
username: formFields.username || event.user.username || event.user.user_metadata?.username,
given_name: formFields.given_name || event.user.given_name || event.user.user_metadata?.given_name,
family_name: formFields.family_name || event.user.family_name || event.user.user_metadata?.family_name,
phone_number: formFields.phone_number || event.user.phone_number || event.user.user_metadata?.phone_number,
email: formFields.email || event.user.email || event.user.user_metadata?.email
};
const filtered = {};
for (const [key, value] of Object.entries(meta)) {
if (value !== undefined && value !== null && value !== '' && value !== 'undefined' && value !== 'null') {
filtered[key] = value;
}
}
console.log('[buildAndSetUserMetadata] Setting user_metadata:', JSON.stringify(filtered));
api.user.setUserMetadata(filtered);
}
function isMissing(v) {
return v == null || v === '' || v === 'undefined' || v === 'null';
}
exports.onExecutePostLogin = async (event, api) => {
console.log('[post_login_wiz4_checkRouting] user_metadata at entry:', typeof event.user.user_metadata, JSON.stringify(event.user.user_metadata));
const userMeta = event.user.user_metadata || {};
console.log('[post_login_wiz4_checkRouting] user_metadata at entry:', JSON.stringify(event.user.user_metadata));
// Check for missing username fields
const needsUsername =
isMissing(userMeta.username) ||
isMissing(userMeta.given_name) ||
isMissing(userMeta.family_name);
// Check for missing phone (standardize to phone_number)
const needsPhone = isMissing(userMeta.phone_number);
// Check for profile completion
const isProfileComplete = userMeta.profile_complete === true;
if (needsUsername) {
console.log('[post_login_wiz4_checkRouting] needsUsername = true, rendering username form');
return api.prompt.render(usernameFormId);
}
if (needsPhone) {
console.log('[post_login_wiz4_checkRouting] needsPhone = true, rendering phone form');
return api.prompt.render(phoneFormId);
}
if (!isProfileComplete) {
console.log('[post_login_wiz4_checkRouting] All data present, rendering done form');
return api.prompt.render(doneFormId);
}
console.log('[post_login_wiz4_checkRouting] All required data present and profile marked complete, no form needed.');
// No form needed, let login proceed
};
exports.onContinuePostLogin = async (event, api) => {
console.log('[post_login_wiz4_checkRouting] user_metadata at entry:', typeof event.user.user_metadata, JSON.stringify(event.user.user_metadata));
// No action needed, but handler must be present
console.log('[post_login_wiz4_checkRouting] onContinuePostLogin called, nothing to do.');
};
const axios = require("axios");
const isMissing = v => !v || v === "undefined" || v === "null";
const doneFormId = 'ap_QRSTU';
const apiErrorFormID = 'ap_FGHIJ';
// Standardized helper for building and setting user metadata
function buildAndSetUserMetadata(api, event, formFields = {}) {
const meta = {
username: formFields.username || event.user.username || event.user.user_metadata?.username,
given_name: formFields.given_name || event.user.given_name || event.user.user_metadata?.given_name,
family_name: formFields.family_name || event.user.family_name || event.user.user_metadata?.family_name,
phone_number: formFields.phone_number || event.user.phone_number || event.user.user_metadata?.phone_number,
email: formFields.email || event.user.email || event.user.user_metadata?.email,
profile_complete: formFields.profile_complete || event.user.user_metadata?.profile_complete
};
const filtered = {};
for (const [key, value] of Object.entries(meta)) {
if (value !== undefined && value !== null && value !== '' && value !== 'undefined' && value !== 'null') {
filtered[key] = value;
}
}
console.log('[buildAndSetUserMetadata] Setting user_metadata:', JSON.stringify(filtered));
api.user.setUserMetadata(filtered);
}
async function callValidationApi(secrets, endpointSecretKey, payload) {
const { ENVIRONMENT, API_HOSTNAME_TEMPLATE } = secrets;
const endpointPathAndKey = secrets[endpointSecretKey];
if (!ENVIRONMENT || !API_HOSTNAME_TEMPLATE || !endpointPathAndKey) {
throw new Error(`Configuration error: A required secret is missing for ${endpointSecretKey}.`);
}
const baseUrl = API_HOSTNAME_TEMPLATE.replace('${environment}', ENVIRONMENT);
const fullUrl = `https://${baseUrl}${endpointPathAndKey}`;
console.log(`Calling completion endpoint: ${fullUrl}`);
try {
const response = await axios.post(fullUrl, payload);
return response.data;
} catch (error) {
console.error(`API call to ${fullUrl} failed: ${error}`);
throw new Error('An external API call failed.');
}
}
exports.onExecutePostLogin = async (event, api) => {
console.log('[post_login_wiz5_markComplete] user_metadata at entry:', typeof event.user.user_metadata, JSON.stringify(event.user.user_metadata));
console.log("in post_login_wiz5_markComplete - onExecute");
// Check if we have all required data
const user = event.user;
const userMeta = user.user_metadata || {};
const hasUsername = !isMissing(userMeta.username);
const hasPhone = !isMissing(userMeta.phone_number);
const isAlreadyComplete = !isMissing(userMeta.profile_complete);
const openId = user && user.user_id;
console.log('[post_login_wiz5_markComplete] onExecutePostLogin START', JSON.stringify(event.user.user_metadata));
console.log(`Completion check: username=${hasUsername}, phone=${hasPhone}, complete=${isAlreadyComplete}`);
if (hasUsername && hasPhone && !isAlreadyComplete) {
try {
console.log("All data collected, calling completion API...");
// Call API to mark user as complete
const result = await callValidationApi(
event.secrets,
'UPDATE_USER_ENDPOINT',
{
UserId: user.user_id,
Username: userMeta.username,
Phone: userMeta.phone_number,
Email: user.email,
FirstName: userMeta.given_name || user.given_name,
LastName: userMeta.family_name || user.family_name,
OpenId: openId,
LoginSource: event.connection.strategy
}
);
console.log(`UPDATE_USER_ENDPOINT result:`, result);
if (result.code === 'PROFILE_COMPLETE' || result === 'OK') {
// Mark profile as complete using standardized helper
buildAndSetUserMetadata(api, event, { profile_complete: true });
console.log('Profile marked as complete');
// Show completion screen
return api.prompt.render(doneFormId);
} else {
console.error('Profile completion failed:', result.userMessage);
return api.prompt.render(apiErrorFormID);
}
} catch (error) {
console.error(`Error in profile completion: ${error}`);
return api.prompt.render(apiErrorFormID);
}
}
console.log("Profile completion not needed at this time");
};
exports.onContinuePostLogin = async (event, api) => {
console.log('[post_login_wiz5_markComplete] user_metadata at entry:', typeof event.user.user_metadata, JSON.stringify(event.user.user_metadata));
console.log('[post_login_wiz5_markComplete] onContinuePostLogin START', JSON.stringify(event.user.user_metadata));
console.log("in post_login_wiz5_markComplete - onContinue");
console.log("Profile setup complete, continuing login");
};
The Form JSON
Here is the current usernam form step(signup_username.json
):
{
"version": "4.0.0",
"form": {
"name": "signup_username",
"languages": {
"primary": "en"
},
"nodes": [
{
"id": "step_LV5P",
"type": "STEP",
"coordinates": {
"x": 500,
"y": 0
},
"alias": "New step",
"config": {
"components": [
{
"id": "rich_text_XarD",
"category": "BLOCK",
"type": "RICH_TEXT",
"config": {
"content": "<h1>Please add your username and contact information</h1>"
}
},
{
"id": "username",
"category": "FIELD",
"type": "TEXT",
"label": "Username",
"required": true,
"sensitive": false,
"config": {
"multiline": false,
"placeholder": "Username",
"min_length": 5,
"max_length": 50
}
},
{
"id": "given_name",
"category": "FIELD",
"type": "TEXT",
"label": "First name",
"required": false,
"sensitive": false,
"config": {
"multiline": false,
"placeholder": "First name",
"min_length": 2,
"max_length": 50
}
},
{
"id": "family_name",
"category": "FIELD",
"type": "TEXT",
"label": "Last name",
"required": false,
"sensitive": false,
"config": {
"multiline": false,
"placeholder": "Last name",
"min_length": 2,
"max_length": 50
}
},
{
"id": "previous_button_PHvK",
"category": "BLOCK",
"type": "PREVIOUS_BUTTON",
"config": {
"text": "Back"
}
},
{
"id": "next_button_H3gh",
"category": "BLOCK",
"type": "NEXT_BUTTON",
"config": {
"text": "Continue"
}
}
],
"next_node": "$ending"
}
}
],
"start": {
"next_node": "step_LV5P",
"coordinates": {
"x": 0,
"y": 0
}
},
"ending": {
"resume_flow": true,
"coordinates": {
"x": 1250,
"y": 0
}
}
},
"flows": {},
"connections": {}
}
Here is the current phone step form (signup_phone.json
):
{
"version": "4.0.0",
"form": {
"name": "signup_phone",
"languages": {
"primary": "en"
},
"nodes": [
{
"id": "step_zbrj",
"type": "STEP",
"coordinates": {
"x": 500,
"y": 0
},
"alias": "New step",
"config": {
"components": [
{
"id": "rich_text_eI6E",
"category": "BLOCK",
"type": "RICH_TEXT",
"config": {
"content": "<h1>Please add your phone number</h1>"
}
},
{
"id": "telephone",
"category": "FIELD",
"type": "TEL",
"label": "Phone",
"required": true,
"sensitive": false,
"config": {
"country_picker": true,
"default_value": "+44",
"strings": {
"filter_placeholder": "+44"
}
}
},
{
"id": "previous_button_qX4J",
"category": "BLOCK",
"type": "PREVIOUS_BUTTON",
"config": {
"text": "Back"
}
},
{
"id": "next_button_rnFl",
"category": "BLOCK",
"type": "NEXT_BUTTON",
"config": {
"text": "Continue"
}
}
],
"next_node": "$ending"
}
}
],
"start": {
"hidden_fields": [
{
"key": "username"
},
{
"key": "given_name"
},
{
"key": "family_name"
}
],
"next_node": "step_zbrj",
"coordinates": {
"x": 0,
"y": 0
}
},
"ending": {
"resume_flow": true,
"coordinates": {
"x": 1250,
"y": 0
}
}
},
"flows": {},
"connections": {}
}