Creating Forms in Nodlin

Feb 3, 2026 ยท 9 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.

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

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"))

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 Input Control

Special control for selecting Nodlin users and groups.

userOpts = webform.nodlinUserOpts(
    help="Select responsible users or groups",
    singleContactOnly=False
)

form.addUserInput('responsible', 'Responsible', userOpts)

# Access selected values
if 'responsible' in value:
    for u in value.responsible['users']:
        print("USER %s in domain %s" % (u['userid'], u['domain']))
    for g in value.responsible['groups']:
        print("GROUP %s in domain %s" % (g['group'], g['domain']))

The result is a dictionary with two lists:

  • users - List of {userid, domain} dictionaries
  • groups - List of {group, domain} dictionaries

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 editor to expand vertically
editorOpts = webform.editorOpts(expandable=True)
form.addEditor("notes", webform.opts(label="Notes"), editorOpts)

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]
)

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
ChatGPT
Authors
ChatGPT
Conversation AI
Conversational AI on almost any topic.