Form Builder

Form Builder

Form Builder is the primary way to create interactive UI components on MelodyArc. When an AI Operator needs a human associate to provide input, make a decision, or review and confirm data, Form Builder delivers the interface that makes that interaction possible.

This is the foundation of human-in-the-loop AI Operator flows — the most common pattern on MelodyArc. An AI Operator handles the logic, but at key decision points, it hands off to an associate through a Form Builder component rendered directly in their workspace.


Dynamic Forms (XML DSL) — Recommended

The XML domain-specific language (DSL) is the current primary method for building Form Builder components. It lets you define rich, interactive forms as a compact XML string — no component configuration objects required. The DSL renders into fully functional UI components that respond to associate input in real time.

Where DSL forms render

DSL forms can be targeted to three UI surfaces:

SurfaceDescription
MACIE Chat ThreadInline within the AI Operator's conversation with the associate
Action PanelThe right-hand panel in the associate workspace
Task Action Drop DownContextual actions available within a task
A DSL form rendered in the Action Panel — an AI Operator requesting associate review before sending a customer message

How it works

DSL markup is embedded in the instructions field of a Form Builder page — the platform renders it as a live interactive component within the surrounding Form Builder UI. This remains the standard pattern until Form Builder is fully deprecated in 2026 (see Legacy Form Builder below). DSL can also be used in any other Form Builder component, either within invoke point attributes or directly in code points.

The example below shows a complete invoke_component Point. The DSL is passed as the instructions value on a page, and associate-provided values are mapped back to the data token via invoke_data_token.

{
  "organization_id": "org_YOUR_ORG_ID",
  "name": "collect_user_feedback",
  "partition": "op_skills",
  "description": "Collect structured feedback from the user via a form_builder UI",
  "tags": ["skill", "ui", "invoke_component", "operator:chat"],
  "inputs": {
    "task.operator": { "evaluate": "=chat" },
    "_this.status": { "evaluate": "=ready" },
    "_this.name": { "evaluate": "=collect_user_feedback" }
  },
  "attributes": {
    "friendly_name": "Collect user feedback",
    "invoke_pages": [
      {
        "header": "Quick Feedback",
        "instructions": "Please fill this out so I can help you better.",
        "fields": [
          {
            "id": "satisfaction",
            "title": "How satisfied are you?",
            "component": "radio_group",
            "instructions": "Choose one option.",
            "required": true,
            "default_value": "good",
            "options": [
              { "value": "great", "label": "Great" },
              { "value": "good", "label": "Good" },
              { "value": "ok", "label": "Okay" },
              { "value": "bad", "label": "Bad" }
            ]
          },
          {
            "id": "topic",
            "title": "What topic is this about?",
            "component": "select",
            "instructions": "Pick the closest match.",
            "required": true,
            "options": "@feedback_topics"
          },
          {
            "id": "details",
            "title": "Anything else you want to add?",
            "component": "text_area",
            "instructions": "Optional, but helpful.",
            "required": false
          }
        ]
      }
    ],
    "invoke_data_token": {
      "request.feedback.satisfaction": "satisfaction",
      "request.feedback.topic": "topic",
      "request.feedback.details": "details"
    },
    "add_keys": {
      "response.user_response": "Thanks — I've got your feedback.",
      "request.feedback.captured_by": "_this.name"
    }
  },
  "code": "invoke_component",
  "allow_taskless": false,
  "is_quicklink": false,
  "allow_dynamic_code": false,
  "allow_dynamic_key": false,
  "version": "latest",
  "search_resource_type": "invoke"
}

DSL example

The screenshot above is produced by the following DSL. It demonstrates the core patterns: state initialization, dynamic variable interpolation via ${}, conditional rendering, and two-way input binding via path. Note that the DSL uses explicit core: namespace prefixes, which is the standard convention.

<core:card padding="lg">
  <state:set path="form.allow_send" value="yes" />
  <state:set path="form.message_rejection_reason" value="" mode="unset" />
  <state:set path="form.message_header" value="${internal_only}" mode="unset" />

  <core:paper
    withBorder="false"
    radius="0"
    p="xl"
    shadow="md"
    style='{
      "background":"#ffffff",
      "border":"1px solid rgba(0,0,0,0.08)",
      "boxShadow":"0 14px 40px rgba(0,0,0,0.10)"
    }'
  >
    <core:stack gap="lg">

      <core:stack gap="xs">
        <core:badge variant="light" color="blue" style='{"width":"fit-content"}'>
          <core:if path="form.message_header" equals="no">
            Customer Facing Message
          </core:if>
          <core:if path="form.message_header" equals="yes">
            Internal Workflow Message
          </core:if>
        </core:badge>
      </core:stack>

      <core:divider style='{"opacity":0.65}' />

      <core:stack gap="sm">
        <core:paper
          radius="0"
          p="md"
          style='{
            "background":"rgba(0,0,0,0.02)",
            "border":"1px solid rgba(0,0,0,0.06)"
          }'
        >
          <core:text size="sm" style='{"whiteSpace":"pre-wrap","lineHeight":"1.65"}'>
            ${message}
          </core:text>
        </core:paper>

        <core:text size="sm" c="dimmed" style='{"lineHeight":"1.55"}'>
          ${reason}
        </core:text>
      </core:stack>

      <core:divider style='{"opacity":0.65}' />

      <core:stack gap="sm">
        <core:text size="sm" fw="600">Can I proceed with this?</core:text>

        <core:radioGroup path="form.allow_send" withAsterisk="true">
          <core:stack gap="xs">
            <core:radio value="yes" label="Yes" size="md" />
            <core:radio value="no" label="No" size="md" />
          </core:stack>
        </core:radioGroup>

        <core:if path="form.allow_send" equals="no">
          <core:paper
            radius="0"
            p="md"
            style='{
              "background":"rgba(255, 0, 0, 0.03)",
              "border":"1px solid rgba(255, 0, 0, 0.12)"
            }'
          >
            <core:textarea
              label="Reason (required)"
              path="form.message_rejection_reason"
              withAsterisk="true"
              autosize="true"
              minRows="5"
              placeholder="Explain what needs to change (tone, facts, missing info, etc.)"
            />
          </core:paper>
        </core:if>
      </core:stack>

    </core:stack>
  </core:paper>
</core:card>

DSL basics

The DSL uses XML-like tags that map to Mantine UI components. Tags use the explicit core: namespace prefix. Dynamic values from the AI Operator's execution context are interpolated using ${} syntax.

State binding — attach any input to a state path using path. The associate's input is read from and written to that path automatically:

<core:textInput label="Email" path="form.email" />
<core:select label="Priority" path="form.priority" data='[{"value":"high","label":"High"},{"value":"low","label":"Low"}]' />

Conditional rendering — show or hide elements based on current state:

<core:if path="form.allow_send" equals="no">
  <core:textarea label="Reason" path="form.rejection_reason" withAsterisk="true" />
</core:if>

Initial values — seed state from markup, with optional mode="unset" to only apply if the path has no value yet:

<state:set path="form.status" value="pending" />
<state:set path="form.notes" value="${operator_notes}" mode="unset" />

Dynamic variable interpolation — inject values from the AI Operator's execution context directly into the markup:

<core:text size="sm">${message_body}</core:text>

Layout — compose with cards, stacks, grids, and groups:

<core:card>
  <core:stack gap="md">
    <core:grid gutter="md">
      <core:gridCol span='{"base":12,"md":6}'>
        <core:textInput label="First name" path="form.firstName" />
      </core:gridCol>
      <core:gridCol span='{"base":12,"md":6}'>
        <core:textInput label="Last name" path="form.lastName" />
      </core:gridCol>
    </core:grid>
  </core:stack>
</core:card>

For a full DSL component reference, see Dynamic Forms.



Legacy Form Builder

Form Builder is invoked from a code point using get_component(). The function returns a configuration object that defines what the associate sees — one or more pages, each with a header, instructions, and a set of named input fields. When the associate completes the form and submits, the platform writes their responses back to the data token using the data_token map, making those values available to the rest of the AI Operator's execution flow.

async function get_component() {
  return {
    component_name: "form_builder",
    friendly_name: _token.friendly_name,
    pages: [
      {
        header: "Confirm Action",
        instructions: "Review and confirm the details below.",
        fields: [
          {
            id: "disposition",
            title: "Proceed?",
            component: "radio_group",
            required: true,
            options: [
              { value: "confirm", label: "Yes, confirm" },
              { value: "cancel", label: "No, cancel" },
            ],
          },
        ],
      },
    ],
    data_token: {
      "temp.disposition": "disposition",
    },
  };
}

Each field declares a named component type that determines which UI element renders. The data_token maps each field's id to a path in the data token where its value will be written. friendly_name sets the label displayed in the associate's task pane.

get_component() and data_token are not going away. What is being retired in 2026 is the named component type system and the rigid page-and-fields structure it requires. The DSL replaces that layer with far more expressive, unconstrained markup. Until then, the component types below remain fully supported.


Supported components

View all legacy Form Builder components

The following components are available within the fields array on any page.

Text Input (text_input)

Captures free-form text with optional validation. Key properties: id, title, component, instructions, default_value, required, reg_ex, placeholder

Textarea (textarea)

Multi-line text input for longer responses. Key properties: id, title, component, instructions, default_value, required, size

Markdown Editor (markdown_editor)

Rich text editor supporting markdown formatting. Key properties: id, title, component, instructions, default_value, required, size

Select (select)

Dropdown list for selecting a single option. Key properties: id, title, component, options, default_value, required, allow_custom

Multi-Select (multi_select)

Dropdown list for selecting multiple options. Key properties: id, title, component, options, default_value, required, allow_custom

Select Tags (select_tags)

Tag-style multi-select input. Key properties: id, title, component, options, default_value, required, allow_custom

Radio Group (radio_group)

Mutually exclusive option selection displayed as radio buttons. Key properties: id, title, component, options, required Options shape: [{ value: string, label: string }]

Checkbox (checkbox)

Single boolean toggle. Key properties: id, title, component, default_value, required

Path Evaluator (path_evaluator)

Evaluates entity paths against defined input criteria. Key properties: id, title, component, paths, default_value, required, allow_custom, instructions

Date Picker (date_picker)

Calendar-based date selection. Key properties: id, title, component, default_value, required

Time Picker (time_picker)

Clock-based time selection. Key properties: id, title, component, default_value, required

File Upload (file_upload)

Accepts file uploads from the associate. Key properties: id, title, component, required, accepted_types

Display Text (display_text)

Read-only text displayed to the associate. Does not capture input. Key properties: id, title, component, content

Display Image (display_image)

Renders an image within the form. Key properties: id, component, url, alt