Skip to main content
Version: Latest (4.9.0)

Form Layout

Guide for creating ServiceNow form layouts using the Form API. Forms define which fields, annotations, formatters, and embedded lists appear on a record's form view and how they are arranged.

When to Use

  • Defining what fields appear on a record form and in what order
  • Organizing fields into sections with one-column or two-column layouts
  • Adding informational annotations (banners, warnings, instructions)
  • Embedding related record lists directly within a form section
  • Adding formatters (activity streams, attached knowledge) to a form
  • Creating role-restricted or user-specific form layouts

Instructions

  1. Choose the view first: Use default_view for the default layout, or create a Record<'sys_ui_view'> for a custom view. The same view const works with both Form() and List().
  2. Organize sections logically: Header/details first, notes/activity in the middle, related lists at the bottom. Use descriptive captions (e.g., 'Assignment', not 'Section 1').
  3. Pick the right layout block: Use two-column for 4+ short fields (dropdowns, references). Use one-column for text areas, formatters, annotations, and embedded lists.
  4. Set the element type: Every element needs a type discriminator: 'table_field', 'annotation', 'formatter', or 'list'.
  5. Import only what you use: Import AnnotationType, Formatter, default_view, or Record only if actually used — unused imports cause build issues.
  6. Avoid duplicates: Each field should appear only once per section.
  7. Dedicate sections to special elements: Formatters and embedded lists work best in their own sections, never mixed with regular fields.

API Reference

See the form-api topic for the full property reference, including FormSection, LayoutBlock, and FormElement types.

Key Concepts

Record Hierarchy

The Form API creates a hierarchy of ServiceNow records automatically:

RecordTableDescription
Formsys_ui_formMain form record linking a table to a view
Form Section Linksys_ui_form_sectionJoin table linking form to sections (with position)
Sectionsys_ui_sectionSection records with caption, header, and view
Elementsys_ui_elementIndividual field elements within sections
Annotationsys_ui_annotationAnnotation records (when type: 'annotation')

You only define sections in the sections array — the Form API creates the link records for you.

Layout Blocks

Each section's content array contains one or more layout blocks:

One-column — elements stacked vertically:

{ layout: 'one-column', elements: [/* elements */] }

Two-column — elements split left/right (maps to .begin_split/.split/.end_split in ServiceNow):

{ layout: 'two-column', leftElements: [/* left */], rightElements: [/* right */] }

A section can contain multiple layout blocks to create mixed layouts (e.g., a two-column block followed by a one-column block for a text area).

Layout Decision Guide

ConditionLayout
1–3 fields in a sectionone-column
4+ short fields of similar lengthtwo-column
Text areas, descriptions, notesone-column always
Formatters and embedded listsone-column in a dedicated section
Fields are logically paired (start/end date)two-column

Element Types

TypeKey PropertiesDescription
table_fieldfield, typeTable column field
annotationannotationId, text, isPlainText?, annotationType?Informational text banner
formatterformatterRef, formatterName?UI formatter (activity stream, knowledge, etc.)
listlistType, listRefEmbedded related list (12M, M2M, or custom)

Annotations

Annotations display informational text not tied to any table field. The Form API creates both a sys_ui_annotation record and a sys_ui_element linking it to the section.

PropertyRequiredDescription
annotationIdYesNow.ID['<unique_key>'] — becomes the annotation record's sys_id
textYesContent (plain text or HTML)
isPlainTextNotrue (default) for plain text, false for HTML
annotationTypeNoAnnotationType constant or Record<'sys_ui_annotation_type'>. Default: AnnotationType.Info_Box_Blue

Available AnnotationType constants (import from @servicenow/sdk/core):

ConstantDescription
AnnotationType.Info_Box_BlueBlue info box (default)
AnnotationType.Info_Box_RedRed info box
AnnotationType.Line_SeparatorHorizontal line
AnnotationType.Plain_TextPlain text (left-padded)
AnnotationType.Section_DetailsSection details
AnnotationType.Section_Plain_TextPlain text in section
AnnotationType.Section_SeparatorSection separator
AnnotationType.TextBasic text

Embedded Lists

Embedded lists display child or associated records directly in a form section. All three types require type: 'list'.

List TypelistRef FormatWhen to Use
12M'<child_table>.<ref_column>'Child table has a reference field pointing to parent
M2M'<join_table>.<ref_column>'Join table connects two tables via reference fields
customRecord<'sys_relationship'> or GUID stringNo reference field, platform tables, or OOB relationships

Always search for existing relationships before creating new sys_relationship records. Platform tables like sys_attachment, sys_audit, and sys_journal_field use polymorphic string fields and always require custom type.

Platform Tables

Platform tables use polymorphic string fields (not reference fields) and always require custom list type:

  • sys_attachment — file attachments
  • sys_audit — audit history
  • sys_journal_field — journal entries (comments, work notes)
  • sys_history_line — history records
  • approval_approver — approval records
  • sys_email — email records
  • wf_activity — workflow activities

Always use listType: 'custom' with a Record<'sys_relationship'> reference for these tables.

Verification Before Adding Embedded Lists

Before adding an embedded list, verify the relationship and target table exist:

  1. Search existing relationships: Query sys_relationship where basic_apply_to=<parent>^basic_query_from=<child>, retrieving sys_id and name (see the query topic).

  2. Verify target table exists: Query sys_db_object where name=<child_table>, retrieving sys_id, name, and label (see the query topic).

  3. Check field structure: Query sys_dictionary where name=<child_table>^element=<ref_field>^internal_type=reference, retrieving element and reference (see the query topic).

This workflow prevents build errors from missing relationships or tables.

Common OOB Relationships

Query sys_relationship for out-of-box relationships using these encoded query filters (see the query topic):

PurposeEncoded Query
Audit Historybasic_query_from=sys_audit
Attachmentsbasic_query_from=sys_attachment
Comments/Work Notesbasic_query_fromLIKEjournal
Approvalsbasic_query_from=approval_approver
Workflowsbasic_query_from=wf_activity

Use the returned sys_id values in listRef for custom type embedded lists.

Migrating from Record API to Form API

When to convert: When the codebase has Record() calls for sys_ui_section + sys_ui_element and the user wants to add new sections, elements, annotations, or formatters.

5-Step Conversion Algorithm:

  1. Extract section metadata from sys_ui_section Record → caption, header, title; map data.view to a view Record const (not string)

  2. Sort sys_ui_element Records by data.position (ascending)

  3. Group elements into layout blocks by processing split markers:

    • .begin_split → flush buffer, collect left until .split, right until .end_split → two-column block
    • Lone .split (no .begin_split) → buffer becomes left, collect right until next split marker → two-column block (stopping marker NOT consumed)
    • .end_split at top level → silently ignored
    • No markers → one-column block
  4. Map element types:

    • sys_ui_element with data.type='field'{ field: data.element, type: 'table_field' }
    • sys_ui_annotation{ type: 'annotation', annotationId, text, isPlainText, annotationType }
    • sys_ui_formatter{ type: 'formatter', formatterRef }
    • Embedded list → { type: 'list', listType, listRef }
  5. Convert formatter GUIDs to constants:

    • 444ea5c6bf310100e628555b3f0739d6Formatter.Activities_Filtered
    • cfa76e850a0a0b1f01446f67c8538d00Formatter.Attached_Knowledge

Formatter GUID → Name lookup (globally available):

  • 444ea5c6bf310100e628555b3f0739d6Formatter.Activities_Filtered
  • cfa76e850a0a0b1f01446f67c8538d00Formatter.Attached_Knowledge

Example conversion:

Before (Record API):

Record({ table: 'sys_ui_section', data: { name: 'details', caption: 'Details', position: 0 } })
Record({ table: 'sys_ui_element', data: { element: 'name', position: 0, type: 'field' } })
Record({ table: 'sys_ui_element', data: { element: '.begin_split', position: 1 } })
Record({ table: 'sys_ui_element', data: { element: 'state', position: 2, type: 'field' } })
Record({ table: 'sys_ui_element', data: { element: '.split', position: 3 } })
Record({ table: 'sys_ui_element', data: { element: 'priority', position: 4, type: 'field' } })
Record({ table: 'sys_ui_element', data: { element: '.end_split', position: 5 } })

After (Form API):

Form({
sections: [{
caption: 'Details',
content: [
{ layout: 'one-column', elements: [{ field: 'name', type: 'table_field' }] },
{ layout: 'two-column', leftElements: [{ field: 'state', type: 'table_field' }], rightElements: [{ field: 'priority', type: 'table_field' }] }
]
}]
})

View Handling

Default view:

import { Form, default_view } from '@servicenow/sdk/core'
Form({ table: 'x_myapp_task', view: default_view, sections: [...] })

Custom view (canonical pattern):

import { Record, Form } from '@servicenow/sdk/core'

const myView = Record({
$id: Now.ID['my_view'],
table: 'sys_ui_view',
data: { name: 'my_custom_view', title: 'My Custom View' },
})

Form({ table: 'x_myapp_task', view: myView, sections: [...] })

The same view const can be passed to List({ view: myView }) — both APIs accept a sys_ui_view Record reference.

Standard Section Layout

Common section organization pattern for task-based forms:

SectionPositionLayoutContent
Header / Details0Two-columnShort fields: name, state, priority
Extended Details1One-columnText areas: description, comments
Activity / Notes2One-columnFormatters: activity stream
Related Lists3One-columnDedicated — never mix with fields

This pattern provides a consistent user experience across applications.

Default Form Design Heuristic

When the user specifies no explicit layout, follow this heuristic:

  1. Single merged section: Combine all fields into one section unless the user explicitly requests multiple sections
  2. Two-column for short fields: Use two-column layout when there are 4+ short fields (dropdowns, references, dates)
  3. One-column for text areas: Always use one-column for long text fields, HTML fields, and journal fields
  4. Dedicated blocks for formatters: Place formatters (activity, knowledge) in their own one-column blocks
  5. Dedicated blocks for embedded lists: Never mix embedded lists with regular fields
  6. Exclude extended/parent table fields: Only include fields from the target table unless explicitly requested

This heuristic ensures forms are readable and follow ServiceNow best practices.

Section Behavior

How sections render on the form:

  • Tabs: If tabs are enabled on the instance, each section appears as a separate tab
  • Ordering: Sections are ordered by their position in the sections array (0-indexed)
  • Empty sections: Sections with an empty content array create a section header with no elements

Examples

Simple Form with Default View

import { Form, default_view } from '@servicenow/sdk/core'

Form({
table: 'x_myapp_task',
view: default_view,
sections: [
{
caption: 'General',
content: [
{
layout: 'one-column',
elements: [
{ field: 'name', type: 'table_field' },
{ field: 'short_description', type: 'table_field' },
{ field: 'active', type: 'table_field' },
],
},
],
},
{
caption: 'Details',
content: [
{
layout: 'one-column',
elements: [
{ field: 'description', type: 'table_field' },
{ field: 'priority', type: 'table_field' },
],
},
],
},
],
})

Two-Column Layout with Annotations and Activity Formatter

import '@servicenow/sdk/global'
import { Form, Record, AnnotationType, Formatter } from '@servicenow/sdk/core'

const detailedView = Record({
$id: Now.ID['detailed-view'],
table: 'sys_ui_view',
data: { name: 'x_myapp_detailed', title: 'Detailed View' },
})

Form({
table: 'x_myapp_task',
view: detailedView,
roles: ['admin', 'itil'],
sections: [
{
caption: 'Overview',
content: [
{
layout: 'one-column',
elements: [
{
type: 'annotation',
annotationId: Now.ID['overview-banner'],
text: 'Complete all required fields before submitting.',
isPlainText: true,
annotationType: AnnotationType.Info_Box_Blue,
},
],
},
{
layout: 'two-column',
leftElements: [
{ field: 'name', type: 'table_field' },
{ field: 'category', type: 'table_field' },
],
rightElements: [
{ field: 'priority', type: 'table_field' },
{ field: 'assigned_to', type: 'table_field' },
],
},
{
layout: 'one-column',
elements: [{ field: 'description', type: 'table_field' }],
},
],
},
{
caption: 'Activity',
content: [
{
layout: 'one-column',
elements: [
{ type: 'formatter', formatterRef: Formatter.Activities_Filtered },
],
},
],
},
],
})

Complete Form with Embedded Lists

import '@servicenow/sdk/global'
import { Form, Record, Formatter } from '@servicenow/sdk/core'

const taskToAuditRel = Record({
$id: Now.ID['task-audit-rel'],
table: 'sys_relationship',
data: {
name: 'Task Audit History',
basic_apply_to: 'x_myapp_task',
basic_query_from: 'sys_audit',
advanced: false,
},
})

const completeView = Record({
$id: Now.ID['complete-view'],
table: 'sys_ui_view',
data: { name: 'x_myapp_complete', title: 'Complete View' },
})

Form({
table: 'x_myapp_task',
view: completeView,
sections: [
{
caption: 'Details',
content: [
{
layout: 'two-column',
leftElements: [
{ field: 'name', type: 'table_field' },
{ field: 'category', type: 'table_field' },
],
rightElements: [
{ field: 'priority', type: 'table_field' },
{ field: 'assigned_to', type: 'table_field' },
],
},
{
layout: 'one-column',
elements: [{ field: 'description', type: 'table_field' }],
},
],
},
{
caption: 'Activity',
content: [
{
layout: 'one-column',
elements: [
{ type: 'formatter', formatterRef: Formatter.Activities_Filtered },
],
},
],
},
{
caption: 'Subtasks',
content: [
{
layout: 'one-column',
elements: [
{ type: 'list', listType: '12M', listRef: 'x_myapp_subtask.parent_task' },
],
},
],
},
{
caption: 'Audit History',
content: [
{
layout: 'one-column',
elements: [
{ type: 'list', listType: 'custom', listRef: taskToAuditRel },
],
},
],
},
],
})

HTML Annotation

Add an annotation with HTML formatting (isPlainText: false) to display rich text with bold, italic, and links.

import '@servicenow/sdk/global'
import { Form, AnnotationType, default_view } from '@servicenow/sdk/core'

Form({
table: 'x_myapp_task',
view: default_view,
sections: [
{
caption: 'Instructions',
content: [
{
layout: 'one-column',
elements: [
{
type: 'annotation',
annotationId: Now.ID['html-annotation'],
text: '<b>Important:</b> Review the <a href="/kb_view.do">knowledge base</a> before proceeding.',
isPlainText: false,
annotationType: AnnotationType.Info_Box_Blue,
},
{ field: 'short_description', type: 'table_field' },
],
},
],
},
],
})

Result: Annotation displays with bold text and clickable link.

Custom Annotation Type

Create a custom annotation type with custom CSS and reference it in the form.

import '@servicenow/sdk/global'
import { Record, Form, default_view } from '@servicenow/sdk/core'

const customAnnotationType = Record({
$id: Now.ID['custom-warning-type'],
table: 'sys_ui_annotation_type',
data: {
name: 'Custom Warning',
style: `
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 12px;
margin: 8px 0;
color: #856404;
`,
},
})

Form({
table: 'x_myapp_task',
view: default_view,
sections: [
{
caption: 'Task Details',
content: [
{
layout: 'one-column',
elements: [
{
type: 'annotation',
annotationId: Now.ID['custom-warning-annotation'],
text: 'This task requires manager approval before proceeding.',
isPlainText: true,
annotationType: customAnnotationType,
},
{ field: 'short_description', type: 'table_field' },
{ field: 'assigned_to', type: 'table_field' },
],
},
],
},
],
})

Result: Annotation displays with custom yellow warning styling.

Multiple AnnotationType

Use different annotation types across multiple sections to demonstrate Plain_Text, Line_Separator, and Section_Details.

import '@servicenow/sdk/global'
import { Form, AnnotationType, default_view } from '@servicenow/sdk/core'

Form({
table: 'x_myapp_task',
view: default_view,
sections: [
{
caption: 'Text Annotations',
content: [
{
layout: 'one-column',
elements: [
{
type: 'annotation',
annotationId: Now.ID['plain-text-annotation'],
text: 'Plain text annotation — no HTML formatting applied.',
isPlainText: true,
annotationType: AnnotationType.Plain_Text,
},
{ field: 'description', type: 'table_field' },
],
},
],
},
{
caption: 'Separators and Section Details',
content: [
{
layout: 'one-column',
elements: [
{
type: 'annotation',
annotationId: Now.ID['line-separator'],
text: '',
isPlainText: true,
annotationType: AnnotationType.Line_Separator,
},
{ field: 'state', type: 'table_field' },
{
type: 'annotation',
annotationId: Now.ID['section-details'],
text: 'Provide additional context about the task below.',
isPlainText: true,
annotationType: AnnotationType.Section_Details,
},
{ field: 'notes', type: 'table_field' },
],
},
],
},
],
})

Result: Demonstrates three different annotation styles in one form.

M2M Embedded List

Many-to-Many relationship using a join table pattern with listType: "M2M" and listRef: "join_table.parent_field" format.

import { Form, default_view } from '@servicenow/sdk/core'

Form({
table: 'sn_form_example_task',
view: default_view,
sections: [
{
caption: 'Details',
content: [
{
layout: 'one-column',
elements: [{ field: 'name', type: 'table_field' }],
},
],
},
{
caption: 'Tags',
content: [
{
layout: 'one-column',
elements: [
{
type: 'list',
listType: 'M2M',
listRef: 'sn_form_example_task_tag.task',
},
],
},
],
},
],
})

How it works: The join table (sn_form_example_task_tag) has a reference field (task) pointing back to the parent. The listRef format is join_table.parent_field.

When showing related records on a form, choose between embedding lists within form sections (Form API) and end-of-form related lists (Record API).

Core principle: Embedded List = "Edit in context" | Related List = "Navigate & manage"

Embedded List (Form API)Related List (Record API)
User needs to edit/act immediately in contextUser needs to view/manage/navigate data
Small dataset (< 20 records)Large or unbounded dataset ("all", "history")
Core workflow actionSecondary/supporting data

Keyword signals:

Signals → Embedded ListSignals → Related List
"quickly", "inline", "while filling", "within section", "immediate", "direct edit""all", "history", "at the bottom", "end of form", "related to", "display on record"
Action verbs: "create", "edit", "modify", "select", "assign"View verbs: "show", "display", "list", "view"

Default: When ambiguous, use Related List (standard ServiceNow behavior). See the relationship-guide topic for end-of-form related list patterns.

Avoidance

  • Never put text areas in two-column blocks — text areas (description, work_notes) need full width. Always use one-column.
  • Never mix embedded lists with regular fields — embedded lists must be in their own dedicated section with a descriptive caption.
  • Never use empty captions — empty caption strings cause build errors. Always provide a meaningful section name.
  • Never hardcode formatter or relationship GUIDs — use Formatter.* constants for Activity and Attached Knowledge. For relationships, use a Record<'sys_relationship'> reference or query the instance for the sys_id.
  • Never use raw GUID strings for annotationType — use AnnotationType.* constants when a built-in type matches. For custom styles, create a Record<'sys_ui_annotation_type'>.
  • Never import unused symbols — importing Record, default_view, AnnotationType, or Formatter when not used causes build issues.
  • Never duplicate fields in the same section — each field should appear only once per section to avoid rendering issues.
  • Never create sys_ui_formatter records for globally available formatters — Activity (Formatter.Activities_Filtered) and Attached Knowledge (Formatter.Attached_Knowledge) already exist on all instances.
  • Never exceed 1000 characters in annotation text — keep annotations concise. For lengthy content, consider using a UI Page or knowledge article link instead.
  • Never use JavaScript or complex HTML in annotations — use plain text (isPlainText: true) by default. Only use HTML for simple formatting (<b>, <br>) when strictly necessary.