Creating Forms in Nodlin

Feb 3, 2026 ยท 16 min read

Overview

Forms in Nodlin provide an interactive interface for users to input and view data associated with nodes. This guide covers the webform module and how to create rich, functional forms.

For complete API details, see the Starlark Extensions Reference.

Basic Form Structure

Every form starts with creating a new webform object and ends by displaying it to the user.

# Create a new form
form = webform.new()

# Add form controls
form.addTextInput("name", webform.opts(label="Name", required=True))

# Form is automatically displayed to user

Form Organization

Groups and Columns

Groups allow you to organize form controls. Columns within groups enable multi-column layouts (12-column grid system).

form = webform.new()

# Create a group for related fields
detailsGroup = form.addGroup("_detailsGroup")

# Add two columns (6 columns each = 50% width)
leftCol = detailsGroup.addColumn("_leftCol", 6)
rightCol = detailsGroup.addColumn("_rightCol", 6)

# Add fields to each column
leftCol.addTextInput("firstName", webform.opts(label="First Name"))
rightCol.addTextInput("lastName", webform.opts(label="Last Name"))

Tabs

Tabs organize content into separate pages within a form.

form = webform.new()

# Create tabs
detailsTab = form.addTab("_detailsTab", "Details")
settingsTab = form.addTab("_settingsTab", "Settings")

# Create groups for each tab
detailsGroup = form.addGroup("_detailsGroup")
settingsGroup = form.addGroup("_settingsGroup")

# Add groups to tabs
detailsTab.addElement("_detailsGroup")
settingsTab.addElement("_settingsGroup")

# Add controls to groups
detailsGroup.addTextInput("name", webform.opts(label="Name"))
settingsGroup.addNumberInput("priority", webform.opts(label="Priority"))

Example from TOGAF Architecture Project:

# Details tab
detailsTab = form.addTab("_detailsTab", "Details")
detailsGroup = form.addGroup("_detailsGroup")
detailsTab.addElement("_detailsGroup")

detailsGroup.addTextInput("name", webform.opts(label="Project Name", required=True))
detailsGroup.addEditor("description", webform.opts(label="Description"))

# Actions tab
actionsTab = form.addTab("_actionsTab", "Actions")
actionsGroup = form.addGroup("_actionsGroup")
actionsTab.addElement("_actionsGroup")

actionsGroup.addStatic("_actionsHeader", "h4", "Create Architecture Artifacts")
actionsGroup.addButton("addStakeholder", "Add Stakeholder", 
    webform.buttonOpts(action="addStakeholder", buttonClass="bg-blue-500"))

Form Controls

Text Input

Basic text input fields.

# Simple text input
form.addTextInput("email", webform.opts(label="Email"))

# With additional options
textOpts = webform.textInputOpts(inputType="email")
form.addTextInput("email", webform.opts(label="Email", required=True), textOpts)

Number Input

Numeric input fields.

form.addNumberInput("age", webform.opts(label="Age (years)", info="Enter your age"))

Editor

Rich text editor for longer content (HTML). Use this for prose descriptions, not code or single-line expressions.

form.addEditor("description", 
    webform.opts(label="Description"),
    webform.editorOpts(expandable=True))

Expression Editor

Code-style editor for Python / Starlark-style expressions (plain text, not HTML). Uses the same CodeMirror theme and syntax highlighting as the Nodlin script panel, with behaviour tuned for single-statement expressions.

Use this when the field is an evaluable expression (for example the built-in expression node). Prefer addTextArea for free-form multi-line notes, and addEditor for rich text.

# Minimal usage (defaults: 3 rows, expandable modal, line wrap on)
form.addExpressionEditor(
    "expression",
    webform.opts(
        label="Expression",
        info="Enter a Python-style expression",
        fieldName="Expression",
    ),
)

# With explicit options
eeOpts = webform.expressionEditorOpts(rows=3, expandable=True)
form.addExpressionEditor(
    "formula",
    webform.opts(label="Formula", info="Single expression only"),
    eeOpts,
)

Behaviour

  • Value type: plain string stored on the node field (same as a textarea). The form field name is the first argument ("expression" above).
  • Inline editor: compact CodeMirror area in the form; height follows rows (default 3).
  • Syntax: Python highlighting and dark theme aligned with the script editor.
  • Line wrap: always enabled (no toggle). Long expressions wrap instead of scrolling horizontally.
  • No line numbers: expressions are expected to be a single statement, so line numbers and fold gutters are omitted.
  • Font size: larger than the full script panel (~50% larger body text) for easier reading in the form.
  • Expandable modal (default on): expand control opens a modal editor for longer edits.
    • Rough size: about 40% of viewport width and 66% of viewport height, with sensible min/max bounds on small screens.
    • Placed right of centre (not full-window centre) so the graph/context remains visible.
    • Done writes the modal value back into the form field and closes the modal.
  • Edit session: typing and opening the expanded editor renew the node edit-activity timer (same pattern as the expandable rich-text editor).
  • Disabled / read-only: respects standard form disabled and readonly state from webform.opts and form context.

Options (webform.expressionEditorOpts)

OptionTypeDefaultDescription
rowsint3Approximate inline height in rows.
expandableboolTrueShow expand control and allow modal editing. Set False for inline-only.

Common field options still come from webform.opts (label, info, required, disabled, align, etc.).

# Inline only (no expand button)
form.addExpressionEditor(
    "quickExpr",
    webform.opts(label="Quick expression"),
    webform.expressionEditorOpts(rows=2, expandable=False),
)

# Taller inline area with modal expand
form.addExpressionEditor(
    "expression",
    webform.opts(label="Expression", required=True),
    webform.expressionEditorOpts(rows=5, expandable=True),
)

Expression node example

The built-in expression form uses this control for the expression body (field name expression), while comment stays a textarea and long description stays a rich editor:

form = webform.new()

exprTab = form.addTab("_exprTab", "Expression")
exprGroup = form.addGroup("_exprGroup")
exprTab.addElement("_exprGroup")

exprGroup.addExpressionEditor(
    "expression",
    webform.opts(
        label="Expression",
        info="Enter expression as a python expression",
        fieldName="Expression",
    ),
    webform.expressionEditorOpts(rows=3),
)

exprGroup.addTextArea(
    "comment",
    webform.opts(label="Comment", info="Short title alternative"),
)

exprGroup.addEditor(
    "description",
    webform.opts(label="Description"),
    webform.editorOpts(expandable=True),
)

exprGroup.addButton(
    "_save",
    "Save",
    webform.buttonOpts(submits=True),
    webform.opts(align="right"),
)

Choosing the right control

NeedControl
Single evaluable expression / formulaaddExpressionEditor
Multi-line plain notesaddTextArea
Rich text / formatted proseaddEditor
Users / groups / domains (ContactList)addUserGroupSelect
Full multi-line scriptsScript panel (not a form control)

Select (Dropdown)

Dropdown selection lists.

# Simple list
statusOptions = ["Open", "In Progress", "Complete"]
form.addSelect("status", statusOptions, webform.opts(label="Status"))

# From TOGAF example
statusOptions = ["Initiation", "Planning", "In Progress", "Review", "Complete", "On Hold"]
detailsGroup.addSelect("status", statusOptions, webform.opts(label="Status"))

Multi-select

Multi-select controls allow users to choose multiple values and (optionally) create new values directly in the form.

# Tuple format: (value, label)
tagOptions = [
    ("ops", "Operations"),
    ("finance", "Finance"),
    ("urgent", "Urgent")
]

msOpts = webform.multiselectOpts(
    search=True,
    closeOnSelect=False
)

form.addMultiselect(
    "tags",
    tagOptions,
    msOpts,
    webform.opts(label="Tags", info="Select one or more tags")
)

Multi-select behavior notes (Nodlin)

  • In the Nodlin form UI, selected options in the dropdown now show a visible tick mark.
  • With create=True, newly created options are selected immediately.
  • If hideSelected=True (Vueform default), selected options are hidden from the dropdown list.
  • For create-enabled option editors, prefer hideSelected=False so users can still see newly created values in the list immediately.
optionEditorItems = [
    ("Open", "Open"),
    ("In Progress", "In Progress"),
    ("Closed", "Closed"),
]

optionEditorOpts = webform.multiselectOpts(
    search=True,
    create=True,
    hideSelected=False,
    appendNewOption=True,
    closeOnSelect=False,
    addOnEnter=True,
    addOnTab=True
)

form.addMultiselect(
    "options",
    optionEditorItems,
    optionEditorOpts,
    webform.opts(label="Options", info="Add or update selectable values")
)

Important: editing existing option lists

When a form is used to edit the allowed options for another field, always initialize the multiselect items from the existing saved option set. Do not start from an empty items list during edit.
If existing values are not seeded, saving can unintentionally replace the option set and downstream nodes may fail validation because previously selected values are no longer present.

Radio Group

Radio button groups for mutually exclusive options.

# Tuple format: (value, label)
severityOptions = [
    ("SEV1", "Critical"),
    ("SEV2", "Major"),
    ("SEV3", "Moderate"),
    ("SEV4", "Minor"),
    ("SEV5", "Informational")
]

form.addRadioGroup("severity", severityOptions,
    webform.radioGroupOpts(view=webform.TABS),
    webform.opts(label="Severity"))

Example from Incident Management:

statusOptions = [
    ("open", "Open"),
    ("in progress", "In progress"),
    ("resolved", "Resolved"),
    ("closed", "Closed")
]

form.addRadioGroup("status", statusOptions,
    webform.radioGroupOpts(view=webform.TABS),
    webform.opts(label="Status", info="Incident status", disabled=True))

Slider

Slider control for numeric ranges.

scoreInputOpts = webform.opts(label="Significance", 
    info="Score from 0 (no impact) to 100 (high impact)")

form.addSlider("weight", 
    webform.sliderOpts(minValue=0, maxValue=100, step=1, 
        tooltips=True, tooltipPosition=webform.BOTTOM),
    scoreInputOpts)

Buttons

Buttons trigger actions or submit forms.

# Submit button
form.addButton("_submit", "Save", 
    webform.buttonOpts(submits=True),
    webform.opts(align="right"))

# Action button
form.addButton("addTask", "Add Task",
    webform.buttonOpts(action="addTask", buttonClass="bg-green-500"),
    webform.opts(align="left"))

Button Layout Example:

buttonGroup = form.addGroup("_buttonGroup")
btnLeftCol = buttonGroup.addColumn("_btnLCol", 3)
btnCenterCol = buttonGroup.addColumn("_btnCCol", 6)
btnRightCol = buttonGroup.addColumn("_btnRCol", 3)

btnLeftCol.addButton("addImpact", "+Impact",
    webform.buttonOpts(action="addImpact", buttonClass="bg-green-accent-3"),
    webform.opts(align="left"))

btnRightCol.addButton("_submit", "Save",
    webform.buttonOpts(submits=True),
    webform.opts(align="right"))

Date and Time Inputs

Date and datetime inputs with flexible formatting.

# Simple date input
form.addDateInput("startDate", webform.opts(label="Start Date"))

# Date with time
dateOpts = webform.dateInputOpts(time=True)
form.addDateInput("reportedAt", 
    webform.opts(label="Reported at", info="Time incident was first reported"),
    dateOpts)

# With format and range
dateOpts = webform.dateInputOpts(
    time=True,
    min='2025-07-01 00:00',
    displayFormat='YYYY-MM-DD'
)
form.addDateInput("eventDate", webform.opts(label="Event Date"), dateOpts)

Date Format Tokens

Formatting follows moment.js tokens:

  • YYYY - 4 digit year
  • MM - Month number
  • DD - Day of month
  • HH - Hour (24h)
  • mm - Minute
  • ss - Second

UTC format: YYYY-MM-DDTHH:mm:ss[Z] (e.g., “2025-07-16T13:45:00Z”)

Example from Incident Management:

# Date inputs in a row
timeGroup = form.addGroup("_timeGroup")
timeLeftCol = timeGroup.addColumn("_trcol", 4)
timeMiddleCol = timeGroup.addColumn("_tmcol", 4)
timeRightCol = timeGroup.addColumn("_tlcol", 4)

myDateOpts = webform.dateInputOpts(time=True)

timeLeftCol.addDateInput("startTime",
    webform.opts(label="Start time", info="Time incident was declared"),
    myDateOpts)

timeMiddleCol.addDateInput("endTime",
    webform.opts(label="End time", info="Time incident was resolved"),
    myDateOpts)

timeRightCol.addDateInput("closedAt",
    webform.opts(label="Closed at", info="Time incident was closed"),
    myDateOpts)

User / group select (addUserGroupSelect)

Standard form control for selecting Nodlin users and groups (and optionally domains). Schema type is user-group-select. The field value is a ContactList object stored on the node UDT โ€” the same shape used elsewhere in Nodlin (actions, FYIs, permissions).

Use this whenever a script needs the user to pick assignees, owners, reviewers, or notification targets. It is the supported replacement for older ad-hoc user pickers.

# Defaults: multi-select, users + groups, no domains
form.addUserGroupSelect(
    "assignees",
    webform.opts(
        label="Assignees",
        info="Select users and/or groups responsible for this work",
    ),
)

# Explicit options
ugOpts = webform.userGroupSelectOpts(
    singleContactOnly=False,  # multi-select (default)
    allowUserOnly=False,      # show groups (default)
    allowDomains=False,       # hide domains (default)
)
form.addUserGroupSelect(
    "reviewers",
    ugOpts,
    webform.opts(label="Reviewers", required=True),
)

# Single user only (no groups, no domains)
form.addUserGroupSelect(
    "owner",
    webform.userGroupSelectOpts(singleContactOnly=True, allowUserOnly=True),
    webform.opts(label="Owner"),
)

Behaviour

  • Value type: ContactList object (not a string, not selection keys).
  • UI: same SelectUserAndGroup control used in actions/FYI panels (chips, recent recipients, preferred users, my groups).
  • Defaults when options are omitted: multi-select; users and groups; domains off.
  • Empty default: { "Users": [], "Groups": [] } so load/save has a stable shape.
  • Disabled / read-only: from webform.opts and form context.

Options (webform.userGroupSelectOpts)

OptionTypeDefaultDescription
singleContactOnlyboolFalseIf True, only one contact can be selected.
allowUserOnlyboolFalseIf True, hide groups (users only).
allowDomainsboolFalseIf True, include domains in the picker.

Common field options still come from webform.opts (label, info, required, disabled, readonly, etc.).

ContactList value shape

# Field name is the first argument to addUserGroupSelect (e.g. "assignees")
contacts = value.assignees if hasattr(value, "assignees") else {}

users = contacts.get("Users", []) if type(contacts) == "dict" else []
groups = contacts.get("Groups", []) if type(contacts) == "dict" else []
domains = contacts.get("Domains", []) if type(contacts) == "dict" else []

for u in users:
    # u = {"userid": "...", "domain": {"domainName": "..."}}
    print("USER", u["userid"], "in", u["domain"]["domainName"])

for g in groups:
    # g = {"groupName": "...", "group_domain": {"domainName": "..."}}
    print("GROUP", g["groupName"], "in", g["group_domain"]["domainName"])

for d in domains:
    # d = {"domainName": "..."}  (only present when allowDomains=True and selected)
    print("DOMAIN", d["domainName"])

JSON shape (as stored / submitted):

{
  "Users": [
    { "userid": "alice", "domain": { "domainName": "acme" } }
  ],
  "Groups": [
    { "groupName": "ops", "group_domain": { "domainName": "acme" } }
  ],
  "Domains": [
    { "domainName": "acme" }
  ]
}

Domains is omitted when empty or when domains are not allowed.

End-to-end: pick contacts, create action + assignment

Combine the control with node.createAction / node.createAssignment (checkpoints required so the action exists before assignment):

# --- form (edit path) ---
form = webform.new()
view = form.addGroup("detailView")

view.addTextInput("title", webform.opts(label="Title", required=True))
view.addUserGroupSelect(
    "assignees",
    webform.userGroupSelectOpts(singleContactOnly=False, allowUserOnly=False, allowDomains=False),
    webform.opts(label="Assignees", info="Who should own this work"),
)
view.addButton(
    "assignWork",
    "Create action & assign",
    webform.buttonOpts(action="assignWork", buttonClass="bg-green-accent-3"),
)
view.addButton("_save", "Save", webform.buttonOpts(submits=True), webform.opts(align="right"))

# --- action handler ---
if type(operation) == "Action" and operation.name == "assignWork":
    contacts = value.assignees if hasattr(value, "assignees") else {}
    users = contacts.get("Users", []) if type(contacts) == "dict" else []
    groups = contacts.get("Groups", []) if type(contacts) == "dict" else []

    if len(users) == 0 and len(groups) == 0:
        fail("Select at least one user or group before assigning")

    # Create the user action on this node (script owner is recorded as creator)
    context.checkpoint()
    due = time.now() + time.parse_duration("24h")
    actionID = node.createAction(
        comment=value.title if hasattr(value, "title") else "Assigned work",
        forDate=due,
        priority=2,
    )

    # One assignment per selected user/group (action must exist first)
    for u in users:
        context.checkpoint()
        node.createAssignment(
            actionid=actionID,
            user=(u["userid"], u["domain"]["domainName"]),
        )
    for g in groups:
        context.checkpoint()
        node.createAssignment(
            actionid=actionID,
            group=(g["groupName"], g["group_domain"]["domainName"]),
        )

Notes:

  • createAction / createAssignment run as pending operations in the current flow. Use context.checkpoint() between create node โ†’ create action โ†’ create assignment when those depend on each other.
  • Assignment takes either user=(userid, domain) or group=(groupName, domain), not both.
  • For a single user without a prior ContactList field, node.assignNewAction(comment=..., user=("alice", "acme")) remains a shorthand.

Choosing the right control (people)

NeedControl
Pick users/groups for a ContactList fieldaddUserGroupSelect
Fixed list of string optionsaddSelect / addMultiselect
Assign work after pickcreateAction + createAssignment (see above)

Lists and Repeating Fields

Create lists of repeating items where users can add/remove entries.

# Create a list container
myList = form.addList("userWeightings")

# Define the object structure for each list item
myObj = myList.setObject()

# Add fields to the repeating object
myObj.addTextInput("label", webform.opts(label="Label"))
myObj.addSlider("weight", 
    webform.sliderOpts(minValue=0, maxValue=100, step=1),
    webform.opts(label="Weight"))
myObj.addHidden("nodeID")  # Hidden field for data storage

Example from Cause/Effect:

# Display causes with significance sliders
if hasattr(node.related, "hasCause"):
    myListOf = form.addList("userWeightings")
    myObj = myListOf.setObject()
    
    # Cause name (read-only)
    myObj.addTextInput("label",
        webform.opts(label="Cause", info="Existing cause for this effect"),
        webform.opts(disabled=True))
    
    # Significance slider
    myObj.addSlider("weight",
        webform.sliderOpts(minValue=0, maxValue=100, step=1, 
            tooltips=True, tooltipPosition=webform.BOTTOM),
        webform.opts(label="Significance"))
    
    # Store node ID
    myObj.addHidden("nodeID")

Static Content

Static Text and HTML

Display read-only content in various HTML elements.

# Headers
form.addStatic("_header", "h2", "Project Details")
form.addStatic("_subheader", "h4", "Configuration Settings")

# Paragraphs
form.addStatic("_info", "p", "Please enter your project information below.")

# Custom HTML
warningHTML = """
<div style='color: orange; padding: 10px; background: #fff3cd;'>
    <strong>Warning:</strong> This action cannot be undone.
</div>
"""
form.addStatic("_warning", "div", warningHTML)

Example from Incident Management:

# Header with columns
titleGroup = form.addGroup("_titleGroup")
titleLeftCol = titleGroup.addColumn("_hrcol", 8, webform.opts(align="left"))
titleRightCol = titleGroup.addColumn("_hlcol", 4)

titleLeftCol.addStatic("_Header", "h2", "Incident")
titleLeftCol.addStatic("_SubHeader", "h4", value.title)

# Warning message
if showImpactWarning:
    warningHTML = """
    <p style="color: orange;">Higher severity impacts exist. 
    Review incident classification: %s</p>
    """ % higherClassifiedImpacts
    form.addStatic("_warning", "div", warningHTML)

Images

Display images in forms using SVG or PNG.

# SVG image
svgIcon = r'<svg width="70" height="70">...</svg>'
form.addImage("_icon", webform.SVG + svgIcon, 
    webform.opts(align="right"))

# PNG image (base64 encoded)
pngData = "data:image/png;base64,iVBORw0KG..."
form.addImage("_logo", webform.PNG + pngData)

Image URI prefixes:

  • webform.SVG - For SVG images
  • webform.PNG - For base64-encoded PNG images

Markdown to HTML

Convert markdown to HTML for display in forms.

myMarkdown = """
# Project Overview
This is the **main project** with the following goals:
- Increase efficiency
- Reduce costs
- Improve quality

## Next Steps
Review the requirements and proceed with implementation.
"""

myHTML = form.markdownToHTML(myMarkdown)
form.addStatic("_overview", "div", myHTML)

Dividers and Spacing

Add visual separation between form sections.

# Horizontal divider
form.addDivider("_div1")

# Or using static HR
form.addStatic("_hr", "hr", "")

# Vertical spacing (number = units)
form.addSpace(2)  # Add 2 units of vertical space

Complete Form Examples

Simple Project Form

form = webform.new()

# Header
form.addStatic("_header", "h2", "New Project")

# Basic fields
form.addTextInput("name", webform.opts(label="Project Name", required=True))
form.addEditor("description", webform.opts(label="Description"))

statusOptions = ["Planning", "Active", "On Hold", "Complete"]
form.addSelect("status", statusOptions, webform.opts(label="Status"))

# Dates
dateOpts = webform.dateInputOpts(time=False)
form.addDateInput("startDate", webform.opts(label="Start Date"), dateOpts)
form.addDateInput("endDate", webform.opts(label="Target End Date"), dateOpts)

# Submit
form.addButton("_submit", "Save Project", 
    webform.buttonOpts(submits=True),
    webform.opts(align="right"))

Multi-Tab Form with Dashboard

This example from TOGAF shows a sophisticated form with tabs and dynamic content.

form = webform.new()

# Details tab
detailsTab = form.addTab("_detailsTab", "Details")
detailsGroup = form.addGroup("_detailsGroup")
detailsTab.addElement("_detailsGroup")

# Project details
detailsGroup.addTextInput("name", webform.opts(label="Project Name", required=True))
detailsGroup.addSpace(1)

detailsGroup.addTextInput("organization", webform.opts(label="Organization"))
detailsGroup.addSpace(1)

statusOptions = ["Initiation", "Planning", "In Progress", "Review", "Complete"]
detailsGroup.addSelect("status", statusOptions, webform.opts(label="Status"))
detailsGroup.addSpace(1)

# Dashboard with HTML
dashboardHTML = """
<div style='padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 
     border-radius: 8px; margin: 16px 0;'>
    <div style='display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;'>
        <div style='background: white; padding: 16px; border-radius: 6px; text-align: center;'>
            <div style='font-size: 32px; font-weight: bold; color: #667eea;'>%s</div>
            <div style='color: #666; font-size: 12px; margin-top: 4px;'>Stakeholders</div>
        </div>
        <!-- More cards... -->
    </div>
</div>
""" % stakeholderCount

detailsGroup.addStatic("_dashboard", "div", dashboardHTML)

# Save button
detailsGroup.addButton("_save", "Save Project",
    webform.buttonOpts(submits=True),
    webform.opts(align="right"))

# Actions tab
actionsTab = form.addTab("_actionsTab", "Actions")
actionsGroup = form.addGroup("_actionsGroup")
actionsTab.addElement("_actionsGroup")

actionsGroup.addStatic("_actionsHeader", "h4", "Create Architecture Artifacts")

actionsGroup.addButton("addStakeholder", "Add Stakeholder",
    webform.buttonOpts(action="addStakeholder", buttonClass="bg-blue-500"))

actionsGroup.addButton("addRequirement", "Add Requirement",
    webform.buttonOpts(action="addRequirement", buttonClass="bg-indigo-500"))

Form Options

Common Options (webform.opts)

These options apply to most form controls:

  • label - Display label for the field
  • info - Help text/tooltip
  • required - Mark field as required
  • disabled - Disable the field (read-only)
  • align - Alignment: “left”, “center”, “right”
opts = webform.opts(
    label="Project Name",
    info="Enter a unique project identifier",
    required=True,
    align="left"
)
form.addTextInput("name", opts)

Button Options

  • submits - Make button submit the form
  • action - Action name to trigger
  • buttonClass - CSS class for styling (e.g., “bg-blue-500”)
# Submit button
submitOpts = webform.buttonOpts(submits=True)

# Action button
actionOpts = webform.buttonOpts(
    action="createTask",
    buttonClass="bg-green-500"
)

Editor Options

  • expandable - Allow the rich-text editor to open in a large modal
  • rows / autogrow - Optional sizing hints for the HTML editor
editorOpts = webform.editorOpts(expandable=True)
form.addEditor("notes", webform.opts(label="Notes"), editorOpts)

Expression Editor Options

webform.expressionEditorOpts(...) supports:

  • rows - Inline height in rows (default 3)
  • expandable - Show modal expand control (default True)

Line wrapping is always on. Line numbers are not shown.

eeOpts = webform.expressionEditorOpts(rows=3, expandable=True)
form.addExpressionEditor(
    "expression",
    webform.opts(label="Expression", info="Python-style expression"),
    eeOpts,
)

Multi-select Options

webform.multiselectOpts(...) supports:

  • inputType - Input type for search text box
  • closeOnSelect - Close dropdown after selecting an option
  • autoComplete - Browser autocomplete mode
  • search - Enable search/filtering
  • create - Allow users to create options not currently in the list
  • hideSelected - Hide selected values from the dropdown list (default behavior in Vueform)
  • appendNewOption - Append created values to available options
  • addOnSpace - Create option on space key
  • addOnTab - Create option on tab key
  • addOnEnter - Create option on enter key
msOpts = webform.multiselectOpts(
    search=True,
    create=True,
    hideSelected=False,
    appendNewOption=True,
    closeOnSelect=False,
    addOnEnter=True,
    addOnTab=True
)

Handling Form Data

Accessing Form Values

Form data is available in the value object after submission:

# Check if field exists
if hasattr(value, "projectName"):
    print("Project:", value.projectName)

# Dictionary-style access
if "status" in value:
    currentStatus = value["status"]

# Safe access with default
projectName = getattr(value, "projectName", "Untitled")

Setting Default Values

Initialize form fields with default values:

defaults = {
    "status": "Planning",
    "priority": "Medium",
    "startDate": time.now(),
    "description": ""
}

for name, defval in defaults.items():
    if not hasattr(value, name):
        value[name] = defval

Handling Button Actions

React to button clicks by checking the operation:

if operation.isActionName("createTask"):
    # Create new task node
    newTaskID = node.linkToNewNode(
        scriptFQN="user.domain.project.task",
        label="New Task"
    )
    value._save = True  # Trigger save after action

if operation.isActionName("addRequirement"):
    # Create requirement
    node.linkToNewNode(
        scriptFQN="user.domain.project.requirement",
        label="requirement"
    )
    value._save = True

Best Practices

1. Organize with Groups and Tabs

For complex forms, use tabs to separate concerns:

# Good: Organized into logical tabs
detailsTab = form.addTab("_details", "Details")
settingsTab = form.addTab("_settings", "Settings")
actionsTab = form.addTab("_actions", "Actions")

Place related fields side-by-side:

dateGroup = form.addGroup("_dateGroup")
startCol = dateGroup.addColumn("_startCol", 6)
endCol = dateGroup.addColumn("_endCol", 6)

startCol.addDateInput("startDate", webform.opts(label="Start Date"))
endCol.addDateInput("endDate", webform.opts(label="End Date"))

3. Provide Helpful Info Text

Guide users with clear descriptions:

form.addTextInput("email",
    webform.opts(
        label="Email",
        info="We'll use this for notifications",
        required=True
    ))

4. Default Values Pattern

Set defaults at the start of your script:

defaults = {
    "title": "",
    "description": "",
    "status": "open",
    "priority": "medium"
}

for name, defval in defaults.items():
    if not hasattr(value, name):
        value[name] = defval

5. Conditional Form Elements

Show fields based on state:

# Only show button if not yet started
if value.startTime == '':
    form.addButton("startTask", "Start Task",
        webform.buttonOpts(action="startTask"))

6. Use Hidden Fields for IDs

Store identifiers without displaying them:

myObj.addHidden("nodeID")  # User won't see this
myObj.addHidden("version")

Troubleshooting

Form Not Displaying

Ensure the form is created and not reassigned:

# Good
form = webform.new()
form.addTextInput("name", webform.opts(label="Name"))

# Bad - overwrites form
form = webform.new()
form = anotherFunction()  # Don't do this!

Values Not Persisting

Check that field names match property names:

# Form field name
form.addTextInput("projectName", webform.opts(label="Project"))

# Access in code - must match!
if hasattr(value, "projectName"):  # Correct
    name = value.projectName

# This won't work:
if hasattr(value, "project_name"):  # Wrong! Use camelCase

Date Format Issues

Use correct format strings:

# UTC format
dateOpts = webform.dateInputOpts(
    time=True,
    valueFormat='YYYY-MM-DDTHH:mm:ss[Z]'  # Note the [Z]
)

Multi-select Options Missing on Re-open

If an options editor multiselect opens with an empty list after data already exists:

  1. Ensure the form seeds addMultiselect(..., items, ...) with current saved options.
  2. Use create=True with appendNewOption=True so newly created values are retained in the options set.
  3. For option-maintenance forms, use hideSelected=False so selected/new options remain visible during editing.

Next Steps

Now that you understand forms:

  1. Review the Python Scripting Guide for overall script structure
  2. Learn about Creating Node Images for visual elements
  3. See complete script examples showing forms in context
John Harrington
Authors
John Harrington
Founder
A technology leader and software architect with 20+ years in financial services and enterprise systems, founder of Nodlin Technologies Ltd๏ฟผ, building connected AI-driven operational intelligence platforms.