Forms in Workflows

Last updated 25 May 2026

A form on the left triggering a workflow on the right, with a Form Respond action writing a custom reply back to the submitter within 20 seconds

If you've used Workflows and Flexie Scripting, everything on this page will feel familiar. It is the same __data notepad and the same kind of listener / action wiring, just specific to forms.

The three form-submit listeners

Every form-submit event lands as a workflow listener you can use as the source of a workflow (the very first step). Which listener applies depends on the form's flavour:

External (public)

Listener key

virtual_workflow.form_submit

Workflow record type

__virtual

Fields land at

__data.form_submission.<key>

Internal, entity-bound

Listener key

form.internal_submit

Workflow record type

The record type the form is bound to (Lead, Contact, Deal, …)

Fields land at

__data.internal_form_submission.data.<key>

Internal, virtual

Listener key

virtual_workflow.internal_virtual_entity_submit

Workflow record type

__virtual

Fields land at

__data.internal_form_submission.data.<key>

When you build a workflow whose source is "Listener" and pick one of these listeners, the builder hides the listeners that don't apply to the workflow's record type, so you can't accidentally pair the wrong listener with the wrong record type.

1. The external (public) form listener

Listener key: virtual_workflow.form_submit Workflow record type: __virtual

What you get in __data

{
  "form_submission": {
    "<form field key>": "<submitted value>",
    "<form field key>": "<submitted value>",
    "attachments": [
      { "...file metadata..." }
    ]
  }
}

Fields are directly under form_submission, with no nested data wrapper and no metadata about the submitter, public submissions are anonymous.

Reading values

{# Form had fields "First Name", "Email", "Notes" #}
Hi {{ __data.form_submission['First Name'] }},

We received your message:
{{ __data.form_submission.Notes }}

We'll reply to {{ __data.form_submission.Email }}.

(Bracket notation for keys with spaces, see Submissions: bracket notation.)

Useful patterns

Create a lead from a public form submission:

  1. Trigger: this listener.
  2. Decision: check {{ __data.form_submission['Email'] }} is not empty.
  3. Action: Create a Lead with field values pulled from __data.form_submission.<key>.
  4. Action: write a custom response back to the submitter, see Writing a custom response.

2. The internal entity-bound form listener

Listener key: form.internal_submit Workflow record type: The record type the form is bound to (Lead, Contact, …).

What you get in __data

{
  "internal_form_submission": {
    "date_added": "2026-05-25 12:34:56",
    "__submitted_from_user_id": 7,
    "__submitted_from_user_full_name": "John Doe",
    "data": {
      "<form field key>": "<submitted value>",
      "<form field key>": "<submitted value>"
    }
  }
}

Notice the two differences versus the external form:

Plus, because the workflow runs on the record the form was opened against, that record's fields are also available at the top level:

Contact's first name (from the record):    {{ first_name }}
Form's first-name field (from submission): {{ __data.internal_form_submission.data['First Name'] }}

Useful patterns

Update the record from the form:

  1. Trigger: this listener.
  2. Action: Update the contact, setting fields from __data.internal_form_submission.data.<key>.

Audit who did it:

{# In a workflow step's "Add a note" action #}
Form "Qualification" submitted by
{{ __data.internal_form_submission.__submitted_from_user_full_name }}
on {{ date(__data.internal_form_submission.date_added, "M j, Y H:i") }}.

3. The internal virtual form listener

Listener key: virtual_workflow.internal_virtual_entity_submit Workflow record type: __virtual

What you get in __data

{
  "internal_form_submission": {
    "__submitted_from_user_id": 7,
    "__submitted_from_user_full_name": "John Doe",
    "data": {
      "<form field key>": "<submitted value>"
    }
  }
}

Same root and same nesting as entity-bound, but no date_added and no current record fields (the workflow runs on the virtual type).

Useful patterns

Spawn a new record from a virtual form:

  1. Trigger: this listener.
  2. Action: Create a Lead / Contact / Case with the fields from __data.internal_form_submission.data.<key>.
  3. Optional: Store the new record's id (__data.__entity_inserted_id) and continue with steps that need it.

Stamp a timestamp yourself:

Because this listener doesn't include date_added, use now() in your first step to capture the moment:

{% set submittedAt = now() %}

…and then write that into any record you create.

Side-by-side: which root, nesting, metadata?

External (public)

Listener key

virtual_workflow.form_submit

Workflow record type

__virtual

__data root

form_submission

Fields nested under

(the root, no data wrapper)

date_added

Submitter user id / name

Top-level record fields

Internal, entity-bound

Listener key

form.internal_submit

Workflow record type

The bound type (Lead, Contact, …)

__data root

internal_form_submission

Fields nested under

data

date_added

Submitter user id / name

Top-level record fields

✓ (the bound record)

Internal, virtual

Listener key

virtual_workflow.internal_virtual_entity_submit

Workflow record type

__virtual

__data root

internal_form_submission

Fields nested under

data

date_added

Submitter user id / name

Top-level record fields

Bookmark this; it is the cheat-sheet that prevents the most common form-workflow mistake (wrong path to the value).

Pushing a form to a user mid-workflow

The "Trigger Internal Form" action

A workflow can send a form to a signed-in user during its execution, then continue once the user submits.

The action's settings:

Setting What it sets
Which form The internal form to push. (Must be an internal form.)
Which user The current owner, a specific user, the user who triggered the workflow, or any rule-based selection.
(Entity-bound forms) Which record The record the submission should attach to, usually the current record the workflow is running on.

The action is immediate, it doesn't queue the step. The form appears for the chosen user in their interface immediately, and the workflow's next step runs once that user submits the form. If you need to time out a long wait (e.g. "if no one fills it in within 24 hours, escalate"), pair this with a restricted-hours-style branch.

There are two action variants:

The builder shows you the right one for the workflow you're in.

Writing a custom response per-submission

This is the pattern that turns a form into a small request/response API: the submitter waits a moment after clicking Submit while the workflow decides what to say, then sees a reply tailored to what they entered.

How it works

  1. The submitter clicks Submit.
  2. Flexie stores the submission and starts the workflow.
  3. The frontend polls for up to 20 seconds waiting for the workflow to write a response.
  4. The workflow has a Form Respond action somewhere in its tree that produces the response.
  5. When that action runs, the response is delivered to the polling submitter and they see it.
  6. If 20 seconds pass with no Form Respond action firing, the form falls back to its default post-submit behaviour (the message or redirect set in the form's Settings).

The "Form Respond" action

The action has two variants:

The settings:

Setting What it does
Response type One of message, redirect, error, json (see below).
Response value The body of the response, computed with Flexie Scripting.

Response shape

There are exactly four response types. Pick the one that matches what the submitter's browser should do:

Response type What the submitter sees Useful for
message A confirmation message shown on the form "Thanks, we'll be in touch with you at {{ __data.form_submission.Email }}."
redirect The browser navigates to a URL Sending them to a thank-you page that depends on what they submitted
error A red error message shown on the form (the form stays open with their input still in place) Rejecting a submission with a reason ("This email is already on file, please log in instead.")
json A raw JSON body When your front-end is calling the form's submit URL via JavaScript and wants the response programmatically rather than as a rendered message

Example, branched custom message

Trigger:  Listener — virtual_workflow.form_submit
Step 1:   Decision — does {{ __data.form_submission['Email'] }}
          match an existing contact?

  Yes ─►  Form Respond (error):
          "An account already exists for that email,
           please reset your password."

  No  ─►  Step 2: Create a Contact
          Step 3: Form Respond (redirect):
          "https://www.yoursite.com/welcome
            ?id={{ __data.__entity_inserted_id }}"

Timing notes

Pairing forms with other workflow building blocks

Forms slot into the same workflow patterns as any other listener trigger:

Quick reference card

EXTERNAL public form
  listener:           virtual_workflow.form_submit
  record type:        __virtual
  read fields as:     {{ __data.form_submission['<key>'] }}
  no submitter info, no date

INTERNAL entity-bound form
  listener:           form.internal_submit
  record type:        the bound type (Lead, Contact, …)
  read fields as:     {{ __data.internal_form_submission.data['<key>'] }}
  read submitter as:  {{ __data.internal_form_submission.__submitted_from_user_full_name }}
  read timestamp as:  {{ __data.internal_form_submission.date_added }}
  top-level fields:   the record's own fields (e.g. {{ first_name }})

INTERNAL virtual form
  listener:           virtual_workflow.internal_virtual_entity_submit
  record type:        __virtual
  read fields as:     {{ __data.internal_form_submission.data['<key>'] }}
  read submitter as:  {{ __data.internal_form_submission.__submitted_from_user_full_name }}
  (no date_added — use now() if you need one)

WORKFLOW ACTIONS
  form.triggerinternal            push an internal entity-bound form to a user
  form.triggerinternal.virtual    push an internal virtual form to a user
  form.form_respond               write custom response (entity-bound)
  virtual_workflow.form_respond   write custom response (external / virtual)

RESPONSE TYPES (for *form_respond actions)
  message    — confirmation message on the form
  redirect   — send the browser to a URL
  error      — red error message, form stays open
  json       — raw JSON body for JS callers

RESPONSE WINDOW
  20 seconds from submit; after that, the form's default response is used.

Back to