Creating Forms in Nodlin
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 userForm 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.optsand form context.
Options (webform.expressionEditorOpts)
| Option | Type | Default | Description |
|---|---|---|---|
rows | int | 3 | Approximate inline height in rows. |
expandable | bool | True | Show 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
| Need | Control |
|---|---|
| Single evaluable expression / formula | addExpressionEditor |
| Multi-line plain notes | addTextArea |
| Rich text / formatted prose | addEditor |
| Users / groups / domains (ContactList) | addUserGroupSelect |
| Full multi-line scripts | Script 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=Falseso 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 yearMM- Month numberDD- Day of monthHH- Hour (24h)mm- Minutess- 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.optsand form context.
Options (webform.userGroupSelectOpts)
| Option | Type | Default | Description |
|---|---|---|---|
singleContactOnly | bool | False | If True, only one contact can be selected. |
allowUserOnly | bool | False | If True, hide groups (users only). |
allowDomains | bool | False | If 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/createAssignmentrun as pending operations in the current flow. Usecontext.checkpoint()between create node โ create action โ create assignment when those depend on each other.- Assignment takes either
user=(userid, domain)orgroup=(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)
| Need | Control |
|---|---|
| Pick users/groups for a ContactList field | addUserGroupSelect |
| Fixed list of string options | addSelect / addMultiselect |
| Assign work after pick | createAction + 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 storageExample 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 imageswebform.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 spaceComplete 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 fieldinfo- Help text/tooltiprequired- Mark field as requireddisabled- 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 formaction- Action name to triggerbuttonClass- 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 modalrows/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 (default3)expandable- Show modal expand control (defaultTrue)
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 boxcloseOnSelect- Close dropdown after selecting an optionautoComplete- Browser autocomplete modesearch- Enable search/filteringcreate- Allow users to create options not currently in the listhideSelected- Hide selected values from the dropdown list (default behavior in Vueform)appendNewOption- Append created values to available optionsaddOnSpace- Create option on space keyaddOnTab- Create option on tab keyaddOnEnter- 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] = defvalHandling 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 = TrueBest 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")2. Use Columns for Related Fields
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] = defval5. 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 camelCaseDate 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:
- Ensure the form seeds
addMultiselect(..., items, ...)with current saved options. - Use
create=TruewithappendNewOption=Trueso newly created values are retained in the options set. - For option-maintenance forms, use
hideSelected=Falseso selected/new options remain visible during editing.
Reference Links
- Starlark Extensions API - Complete API reference
- Python Scripting Guide - Overall scripting guide
- Image Creation Guide - Creating node visuals
Next Steps
Now that you understand forms:
- Review the Python Scripting Guide for overall script structure
- Learn about Creating Node Images for visual elements
- See complete script examples showing forms in context