Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.morf.health/docs/llms.txt

Use this file to discover all available pages before exploring further.

This doc is for anyone building workflows that react to patient form submissions. When a patient submits a form step, Morf receives that data and needs to know what to do with it. These expressions help you control exactly when those actions fire, so the right data reaches the right destination at the right time. To check out our existing destinations, see our Integrations page.

Gate on a specific form step

Use this when you only want an action to fire on a specific step of a multi-step form, not every time the patient submits anything. Start here — match a single step by name:
step_id.orValue("") == "[STEP NAME]"
Match any step that contains a phrase:
step_id.contains("[PHRASE]")
Match any of several steps:
step_id in ["[STEP 1]", "[STEP 2]", "[STEP 3]"]
Examples:
step_id.orValue("") == "Insurance Details"

step_id.contains("Child #2")

step_id in ["Q4_LET'S START WITH THE BASICS", "Q4B_FREE CALL INFO"]
Step names must match exactly. Copy-paste the step name from your form builder — including capitalization, spaces, and punctuation. If it doesn’t match, the filter won’t fire.
The field name step_id is used throughout this doc and is the standard for Morf forms. Other form tools (like Formsort) may use a different field name — check your event payload if step_id isn’t populated.
Use these sample prompts with Flo to generate a CEL expression that fits your goals.
Only trigger this action when the patient reaches the Insurance Details step.
Fire on any step that has 'Child' in its name.

Check that key fields were submitted

Use this when you want to make sure the patient actually filled in certain fields before triggering a downstream action. Single field:
answers.?[FIELD NAME].hasValue()
Multiple required fields (all must be present):
answers.?[FIELD 1].hasValue() && answers.?[FIELD 2].hasValue()
At least one of two fields is present:
answers.?[FIELD 1].hasValue() || answers.?[FIELD 2].hasValue()
Examples:
answers.?patient_first_name.hasValue()

answers.?insurance_holder_relationship.hasValue() && answers.?insurance_member_id.hasValue()

answers.?parent_first_name.hasValue() || answers.?parent_first_name_sp.hasValue()

Route based on a specific answer

Use this when you want the workflow to take a different path depending on what the patient answered — for example, sending them down an insurance path vs. a cash pay path. Template:
answers.?[FIELD NAME].orValue("") == "[EXPECTED VALUE]"
Template — answer is NOT a specific value:
answers.?[FIELD NAME].orValue("") != "[VALUE TO EXCLUDE]"
Examples:
answers.?is_ineligible.value() == "True"

answers.?insurance_payer_name.orValue("") == "cash_pay"

answers.?provider_match.orValue("") != "success"

get_current_property_value("sms_opt_in").orValue(false) == true
Use these sample prompts with Flo to generate a CEL expression that fits your goals.
Route to a different branch if the patient selected 'cash_pay' as their insurance.
Only continue if the patient opted in to SMS.

Prevent duplicate processing

Use this when you want to make sure an action — like creating a HubSpot contact — only happens once, even if the workflow runs multiple times for the same patient. Only run if a property has NOT been set yet:
!get_current_property_value("[PROPERTY]").hasValue()
Only run if a flag is not already true:
!get_current_property_value("[FLAG PROPERTY]").orValue(false)
Only run if this is the first time through (not a duplicate form submission):
!answers.?is_duplicate_found.hasValue() ||
answers.?is_duplicate_found.value() != "true"
Examples:
!get_current_property_value("parent_hubspot_id").hasValue()

!get_current_property_value("has_reached_booking_step").orValue(false)

!get_current_property_value("abandoned_form").hasValue()

Route based on eligibility

Use this when you want to send eligible and ineligible patients down different branches based on their insurance or screener results. Start here — single eligibility check: Patient is eligible:
get_current_property_value("screener_completion_status").hasValue() &&
get_current_property_value("screener_completion_status").value() == "eligible"
Patient has insurance but it’s not Medicaid:
get_current_property_value("primary_insurance").?payer_name.optMap(payer,
  payer != "Medicaid"
).orValue(false)
Patient is ineligible:
answers.?is_ineligible.hasValue() && answers.?is_ineligible.value() == "True"
Advanced — multiple conditions combined:
get_current_property_value("has_diagnosis_report").optMap(diag,
  diag == "No"
).orValue(false)
&&
get_current_property_value("primary_insurance").?payer_name.optMap(primaryPayer,
  primaryPayer != "Medicaid"
).orValue(false)
&&
(
  get_current_property_value("secondary_insurance").?payer_name.optMap(secondaryPayer,
    secondaryPayer != "Medicaid"
  ).orValue(true)
)
Annotated breakdown:
get_current_property_value("has_diagnosis_report")  // Read the diagnosis report property
  .optMap(diag,
    diag == "No"                                    // Pass if value is "No"
  ).orValue(false)                                  // Missing property → fail this check
&&
get_current_property_value("primary_insurance")     // Read the primary insurance object
  .?payer_name                                      // Safely access the payer name field
  .optMap(primaryPayer,
    primaryPayer != "Medicaid"                      // Pass if payer is not Medicaid
  ).orValue(false)                                  // Missing insurance or payer → fail this check
&&
(
  get_current_property_value("secondary_insurance") // Read the secondary insurance object
    .?payer_name                                    // Safely access the payer name field
    .optMap(secondaryPayer,
      secondaryPayer != "Medicaid"                  // Pass if payer is not Medicaid
    ).orValue(true)                                 // No secondary insurance → pass this check
)
Use these sample prompts with Flo to generate a CEL expression that fits your goals.
Only continue if the patient's primary insurance is not Medicaid.
Route to the eligible branch if screener_completion_status is 'eligible'.

Handle multilingual or optional fields with fallbacks

Use this when your form has two versions of the same field — like English and Spanish — and you want to use whichever one the patient filled out. Template:
answers.?[FIELD 1].or(answers.?[FIELD 2])
Examples:
answers.?parent_first_name.or(answers.?parent_first_name_sp)

answers.?parent_last_name.or(answers.?parent_last_name_sp)

answers.?phone_number.or(answers.?phone_number_sp)

answers.?unformatted_primary_insurance_payer_name.or(answers.?primary_insurance_payer_name)
This works for action arguments too, not just filters. Use it anywhere you’re passing form data to an integration.

Check if a linked external ID exists

Use this when you’re about to write data to a third-party tool and need to confirm the patient has already been connected to that platform.
morf_profile_ids.?hubspot.?id.hasValue() && get_current_property_value("hubspot_deal_id").hasValue()
morf_profile_ids.healthie.?id.hasValue() || step_id == "Booking in progress"
Check for the platform ID before creating or updating records. If the patient hasn’t been linked to HubSpot or Intercom yet, the action won’t have the ID it needs and will fail. Add this check in a filter node before any CRM action.

Ready to build?

Open the Dashboard

Start building your form workflow now.

Talk to the Morf team

Want a walkthrough or help getting started? Book a demo with us.

Further reading

For the complete CEL function reference — including all available operators, string functions, date helpers, and optional chaining syntax — see the CEL Reference.