Skip to main content
Version: 4.8.1

Tables, Columns, and Relationships Guide

Guide for creating ServiceNow Tables (sys_db_object), Columns (sys_dictionary), and Relationships (sys_relationship) to define data models. Use when the user mentions tables, columns, fields, schema, extending tables, relationships, related lists, or data modeling.

When to Use​

  • When creating new tables to store application data
  • When adding columns (fields) to new or existing tables
  • When extending an existing table (e.g., extending task)
  • When setting up relationships between tables or configuring related lists
  • When defining choices, defaults, or dynamic values for fields

For field enforcement rules: Use column properties (mandatory, readOnly) for unconditional rules that always apply. For conditional enforcement, choose based on where the rule applies:

  • Data policies — server-side enforcement on both UI and API, applies regardless of how the record is created/updated. See the data-policy-guide topic.
  • UI policies — client-side enforcement in forms only, for dynamic field visibility, mandatory, and read-only rules based on form state.
  • Business rules — server-side logic for complex validation, field calculations, or enforcement that requires scripting beyond simple mandatory/read-only rules. See the business-rule-guide topic.

For automatic field population: Use column default for static, unconditional defaults. For conditional population based on other field values, choose based on the mechanism:

  • Data Lookup — deterministic, no-script field population: "when field X is Y, set field Z to A". Values are maintained as data rows in a matcher table. See the data-lookup-guide topic.
  • Assignment rules — automatic population of assignment_group or assigned_to on task-inherited tables. See the assignment-rule-guide topic.
  • Business rules — scripted field population requiring calculations, external API calls, or logic beyond simple field matching. See the business-rule-guide topic.

Key Concepts​

  • Tables define the data model. Columns define the fields. Relationships connect tables and display related records as lists on forms.
  • Implicit vs explicit relationships: If a reference field exists between tables, the relationship is implicit (no extra record needed). If not, create an explicit sys_relationship record.
  • To access the table using the Table API, allowWebServiceAccess must be enabled.

Instructions​

Creating a New Table​

  1. Table naming: The name must start with the application scope prefix (e.g., x_acme_my_table). The exported variable name MUST match the name property exactly.
  2. Choose column types carefully: Only use supported column types. Only import the types you use to avoid build errors.
  3. Extending tables: Use the extends property to inherit all fields from a base table. Only extend tables marked as extensible.
  4. Cross-scope access: Set accessibleFrom, callerAccess, and actions based on whether other scoped apps need access.
  5. Enable web service access: If the table will be accessed via the Table API (/api/now/table) or any REST integration, set allowWebServiceAccess: true. This defaults to false -- without it, REST calls return 403 "User Not Authorized" even when ACLs are correctly configured.

Field and Table Naming Conventions​

Table and column names must follow ServiceNow's scope-based naming rules. These are enforced by the build — the table plugin emits diagnostics (errors) when they are violated, so a non-conforming name fails the build rather than silently misbehaving. Table names are limited to max length of 30.

Character rules (all scopes)​

  • Table name — lowercase letters, numbers, and underscores only, and must end with a letter or number (not an underscore). Violation: "Table name must only contain lowercase letters, numbers, and underscores and end with a letter or number."
  • Column name — lowercase letters, numbers, and underscores only. Violation: "Column name must only contain lowercase letters, numbers, and underscores."

The ownership prefix​

Every scope has an ownership prefix that marks records it creates:

Current scopePrefixExample
Named scope (e.g. x_acme_app)<scope>_x_acme_app_
Globalu_u_
sn/now scopes (scope name starts with sn or now)—exempt from prefix checks

The core rule: a column needs an ownership prefix only when you add it to an existing table your current scope does not own. When you create a new table, or add columns to a table already in your scope, the columns need no prefix.

  • New table (you are defining the table in this file): its columns need no prefix in any scope — the table itself carries ownership. (A prefix on the columns is still accepted, just not required.)
  • Existing table in your own scope (its name starts with <scope>_): columns need no prefix.
  • Existing table your scope does not own (a base/OOB table, or a table from a different scope): columns must carry your prefix — <scope>_ in a named custom scope, u_ in global or Store-app scope.

Table name rules​

  • Named scope: a new table's name must start with <scope>_. Violation: "'name' property should start with scope prefix '<scope>_'."
  • Global scope: if the table defines any column that is not u_-prefixed, the table name itself must start with u_. Violation: "Global table 'name' property should start with custom prefix 'u_'."
  • sn/now scopes: exempt — no prefix required on the table name.
  • ** max length ** is 30 characters

Column name rules​

Whether a column needs the prefix depends on whether you own the table it belongs to:

  • New table, or existing table already in your scope (name starts with <scope>_, or a u_ table you are creating): columns need no prefix — name, status, etc. are fine.
  • Named scope, table you don't own (e.g. augmenting incident): every column must be prefixed with <scope>_. Violation: "Column name should be prefixed with scope '<scope>_' if table name does not contain prefix" (or, when using augments, "Column name '<name>' must be prefixed with '<scope>_' when augmenting a table").
  • Global scope, table you don't own (e.g. adding fields to OOB incident): every column must be u_-prefixed. Violation: "Column name should be prefixed with 'u_' custom prefix if table name does not contain this prefix, such as when adding columns to an existing global table."
  • Store-app scope: columns always use the u_ prefix.
  • sn/now scopes: exempt — columns need no prefix.

Maint-user exception (platform): the server name check (getColumnPrefixForUser) returns no required prefix for a maint user adding columns to a non-store global table. This does not apply to normal app development — write u_ as above.

Never name a custom field with the sys_ prefix. sys_ is reserved for ServiceNow's system fields (sys_id, sys_created_on, sys_updated_by, etc.). Defining your own sys_-prefixed column collides with platform-managed fields and can corrupt records or break inheritance. Use your ownership prefix instead (u_ in global, <scope>_ in a named scope).

// Named scope x_acme_app — table carries the scope prefix, so columns are bare
export const x_acme_app_widget = Table({
name: 'x_acme_app_widget',
schema: {
name: StringColumn({ label: 'Name' }),
status: ChoiceColumn({ label: 'Status', choices: { /* ... */ } }),
},
})

// Global scope — the new table carries the u_ prefix; its own columns are bare
export const u_widget = Table({
name: 'u_widget',
schema: {
name: StringColumn({ label: 'Name' }),
status: ChoiceColumn({ label: 'Status', choices: { /* ... */ } }),
},
})

// Adding a column to OOB `incident` from a named scope — column needs the scope prefix
export const incident = Table({
augments: 'incident',
schema: {
x_acme_app_risk_score: IntegerColumn({ label: 'Risk Score' }),
},
})

// Adding a column to OOB `incident` from global — column needs the u_ prefix
export const incident = Table({
augments: 'incident',
schema: {
u_risk_score: IntegerColumn({ label: 'Risk Score' }),
},
})

The exported variable name must match the table's identifying name exactly (the name property, or the augments target). A mismatch fails the build: "Table definition should be exported as a named export with the name '<name>'."

Adding Columns to an Existing Table​

  1. When adding columns to a table in a different scope, set augments to the target table name on the Table definition (e.g., augments: 'incident'). Only augments and schema are configurable in this mode — name is not used. Prefix column names with your ownership prefix (<scope>_ in a named scope, u_ in global) per the naming rules above. See the table-augments-guide topic for details.
  2. Import only the column types you actually use -- unused imports cause build errors.

Setting Up Relationships​

  1. Reference field exists between tables: Use implicit relationship (no sys_relationship needed).
  2. No reference field or custom query logic needed: Create explicit sys_relationship record.
  3. Adding existing platform relationship (e.g., Attachments): Use known REL: ID directly.

Avoidance​

  1. Never mismatch the exported variable name and the name property -- they must be identical.
  2. Never use unsupported column types -- only use types listed in the Column Types section.
  3. Never mix basic and advanced relationship fields in the same record.
  4. Never omit the wrapper in query_with scripts -- must use (function refineQuery(current, parent) { ... })(current, parent); format.
  5. Never hardcode sys_id strings in record references -- use ${record.$id}.
  6. Never omit allowWebServiceAccess when the table will be accessed via REST -- it defaults to false, and the Table API will return 403 even with correct ACLs.
  7. Never omit the ownership prefix when you don't own the table -- a new table's name must carry the prefix (<scope>_ in a named scope, u_ in global), and columns added to a table your scope does not own must carry it too. Columns on a table you own (new, or already prefixed for your scope) need no prefix. See Field and Table Naming Conventions. The build enforces this.
  8. Never name a custom field with the sys_ prefix -- sys_ is reserved for ServiceNow's system fields. Use your ownership prefix (u_ or <scope>_) instead.

Table API Reference​

For the full property reference and examples, see the table-api topic.


Column Type Reference​

Column Type Selection Guide​

DataColumn TypeNotes
Short textStringColumnmaxLength < 254 renders single-line
Long textMultiLineTextColumn or StringColumnmaxLength >= 255 renders multi-line
Dropdown/choicesChoiceColumn or StringColumn with choices + dropdownStringColumn allows free-text too
True/falseBooleanColumn
Whole numbersIntegerColumnSupports min and max
Decimal numbersDecimalColumn
Floating pointFloatColumnUse scale for decimal places
Foreign keyReferenceColumnSet referenceTable
Multi-value referenceListColumnSet referenceTable
Multi-select pickerSlushBucketColumnSet script for available items
Date onlyDateColumnFormat: yyyy-mm-dd
Date and timeDateTimeColumnFormat: yyyy-mm-dd HH:mm:ss
DurationDurationColumnDefault: { days, hours, minutes, seconds }
Time of dayTimeColumnDefault: { hours, minutes, seconds }
HTML/rich textHtmlColumnUse html() helper for defaults
URLUrlColumnMust be http:// or https://
EmailEmailColumnMust be user@domain format
JSONJsonColumnUse json() helper for defaults
Field listFieldListColumnNeeds dependent or attributes.table
Server scriptScriptColumn
Radio buttonsRadioColumnRequires choices

All Supported Column Types​

ListColumn, RadioColumn, StringColumn, MultiLineTextColumn, ApprovalRulesColumn, SlushBucketColumn, ChoiceColumn, ScriptColumn, BooleanColumn, ConditionsColumn, DecimalColumn, FloatColumn, IntegerColumn, VersionColumn, DomainIdColumn, FieldNameColumn, FieldListColumn, ReferenceColumn, RecordsColumn, TableNameColumn, UserRolesColumn, BasicImageColumn, DocumentIdColumn, DomainPathColumn, TranslatedTextColumn, SystemClassNameColumn, TranslatedFieldColumn, GenericColumn, DateColumn, DateTimeColumn, CalendarDateTimeColumn, BasicDateTimeColumn, DueDateColumn, IntegerDateColumn, ScheduleDateTimeColumn, OtherDateColumn, DurationColumn, TimeColumn, DayOfWeekColumn, DaysOfWeekColumn, JsonColumn, NameValuePairsColumn, HtmlColumn, UrlColumn, EmailColumn, TemplateValueColumn, GuidColumn.

Only import the types you use.

Common Column Properties​

NameTypeDescription
labelString or ArrayDisplay label for the column.
maxLengthNumberMaximum value length. Default: 40.
activeBooleanDisplay in lists and forms. Default: true.
mandatoryBooleanRequire a value to save. Default: false.
readOnlyBooleanPrevent editing. Default: false.
defaultAnyDefault value when creating a record.
choicesObjectChoices for ChoiceColumn and compatible types.
dropdownString'none', 'dropdown_without_none', 'dropdown_with_none', 'suggestion'.
attributesObjectDictionary attributes.
dynamic_value_definitionsObjectDynamic defaults: calculated_value, dynamic_default, dependent_field, choices_from_other_table.
function_definitionStringGlide function definition (e.g., 'glidefunction:concat(...)').

ReferenceColumn Specifics​

ReferenceColumn({
label: 'Assigned To',
referenceTable: 'sys_user',
cascadeRule: 'none', // 'none' | 'cascade' | 'delete' | 'restrict' | 'clear'
})

Filtering reference values​

Use useReferenceQualifier to control how the picker is filtered:

  • 'simple' — pair with referenceQual holding an encoded query.
  • 'advanced' — pair with referenceQual holding a javascript: scripted qualifier.
  • 'dynamic' — pair with dynamicRefQual pointing at a sys_filter_option_dynamic record.

referenceQual and dynamicRefQual are mutually exclusive.

// Simple encoded query — only show active ITIL users
ReferenceColumn({
label: 'Assigned To',
referenceTable: 'sys_user',
useReferenceQualifier: 'simple',
referenceQual: 'active=true^roles=itil',
})

// Advanced javascript: qualifier — context-aware filter
ReferenceColumn({
label: 'Approver',
referenceTable: 'sys_user',
useReferenceQualifier: 'advanced',
referenceQual: "javascript:'department=' + current.getValue('department')",
})

// Dynamic qualifier — dynamicRefQual takes a reference to a sys_filter_option_dynamic
// record. See the `now-ref` guide for record-reference forms.
ReferenceColumn({
label: 'Owner',
referenceTable: 'sys_user',
useReferenceQualifier: 'dynamic',
dynamicRefQual: refToSysFilterOptionDynamic,
})

Note: catalog variables (ReferenceVariable, ListCollectorVariable) support reference qualifiers but use different property names — see the ReferenceVariable and ListCollectorVariable topics. For an inherited column on an extended table, use OverrideColumn and its referenceQualifier property instead — see the OverrideColumn topic for details.

Cross-Scope Column Pattern​

Use augments to add columns to a table owned by another scope. See the table-augments-guide topic for full instructions.

export const incident = Table({
augments: 'incident',
schema: {
x_acme_my_column: StringColumn({ label: 'My Custom Field' }),
},
})

Determine the Relationship Path​

  1. Reference field exists between tables: Implicit relationship (no sys_relationship needed).
  2. No reference field or custom query: Explicit relationship (create sys_relationship record).
  3. Existing platform relationship: Use known REL: ID.

Common Platform Relationship IDs​

  • Attachments: REL:b9edf0ca0a0a0b010035de2d6b579a03
  • Applications with Role: REL:66c422fac0a80a880012fadcb8c2480e
  • Approval History: REL:247c6f15670303003b4687cb5685ef32

Implicit Relationship Example​

// Table A has a ReferenceColumn pointing to Table B
// Only need sys_ui_related_list + sys_ui_related_list_entry

const listRecord = Record({
$id: Now.ID['department_related_list'],
table: 'sys_ui_related_list',
data: { name: 'department', view: 'Default view' },
});

Record({
$id: Now.ID['department_related_list_entry'],
table: 'sys_ui_related_list_entry',
data: {
list_id: listRecord.$id,
position: '0',
related_list: 'custom_task.department', // table.reference_field format
},
});

Explicit Relationship Example​

// No reference field -- need sys_relationship + sys_ui_related_list + sys_ui_related_list_entry

export const departmentRel = Record({
$id: Now.ID['department_rel_id'],
table: 'sys_relationship',
data: {
advanced: false,
basic_apply_to: 'sn_foo_department',
basic_query_from: 'sn_foo_student',
name: 'Department Allocation Relationship',
query_with: `(function refineQuery(current, parent) {
current.addQuery('department', parent.id);
})(current, parent);`,
simple_reference: false,
},
});

const listRecord = Record({
$id: Now.ID['department_related_list_id'],
table: 'sys_ui_related_list',
data: {
name: 'sn_foo_department',
view: 'Default view',
},
});

Record({
$id: Now.ID['department_related_list_entry_id'],
table: 'sys_ui_related_list_entry',
data: {
list_id: listRecord.$id,
position: '0',
related_list: `REL:${departmentRel.$id}`,
},
});

Relationship Properties (sys_relationship)​

FieldTypeRequiredDescription
namestringNoDescriptive name
basic_apply_toTableNameNoParent table (basic mode)
basic_query_fromTableNameNoChild table (basic mode)
reference_fieldFieldNameNoReference field for the relationship
query_withScriptNoScript to refine the query
advancedBooleanNoWhether advanced mode (default: false)
simple_referenceBooleanNoWhether simple reference relationship

One sys_ui_related_list per table, multiple sys_ui_related_list_entry records for different relationships:

const productContainer = Record({
$id: 'products_related_lists',
table: 'sys_ui_related_list',
data: { name: 'sn_product_life_products', view: 'Default view' },
});

Record({
$id: 'feature_requests_entry',
table: 'sys_ui_related_list_entry',
data: { list_id: productContainer.$id, position: 0, related_list: 'feature_requests.product' },
});

Record({
$id: 'testing_reports_entry',
table: 'sys_ui_related_list_entry',
data: { list_id: productContainer.$id, position: 1, related_list: 'testing_reports.product' },
});

query_with Script Format​

(function refineQuery(current, parent) {
current.addQuery('field', parent.field);
})(current, parent);

The current variable represents the child table query. The parent variable represents the record on the form where the related list appears.