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-guidetopic. - 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-guidetopic.
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-guidetopic. - Assignment rules — automatic population of
assignment_grouporassigned_toon task-inherited tables. See theassignment-rule-guidetopic. - Business rules — scripted field population requiring calculations, external API calls, or logic beyond simple field matching. See the
business-rule-guidetopic.
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_relationshiprecord. - To access the table using the Table API,
allowWebServiceAccessmust be enabled.
Instructions
Creating a New Table
- Table naming: The name must start with the application scope prefix (e.g.,
x_acme_my_table). The exported variable name MUST match thenameproperty exactly. - Choose column types carefully: Only use supported column types. Only import the types you use to avoid build errors.
- Extending tables: Use the
extendsproperty to inherit all fields from a base table. Only extend tables marked as extensible. - Cross-scope access: Set
accessibleFrom,callerAccess, andactionsbased on whether other scoped apps need access. - Enable web service access: If the table will be accessed via the Table API (
/api/now/table) or any REST integration, setallowWebServiceAccess: true. This defaults tofalse-- 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 scope | Prefix | Example |
|---|---|---|
Named scope (e.g. x_acme_app) | <scope>_ | x_acme_app_ |
| Global | u_ | 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
namemust 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 tablenameitself must start withu_. Violation: "Global table 'name' property should start with custom prefix 'u_'." sn/nowscopes: 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 au_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 usingaugments, "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 beu_-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/nowscopes: 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 — writeu_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
nameproperty, or theaugmentstarget). A mismatch fails the build: "Table definition should be exported as a named export with the name '<name>'."
Adding Columns to an Existing Table
- When adding columns to a table in a different scope, set
augmentsto the target table name on theTabledefinition (e.g.,augments: 'incident'). Onlyaugmentsandschemaare configurable in this mode —nameis not used. Prefix column names with your ownership prefix (<scope>_in a named scope,u_in global) per the naming rules above. See thetable-augments-guidetopic for details. - Import only the column types you actually use -- unused imports cause build errors.
Setting Up Relationships
- Reference field exists between tables: Use implicit relationship (no
sys_relationshipneeded). - No reference field or custom query logic needed: Create explicit
sys_relationshiprecord. - Adding existing platform relationship (e.g., Attachments): Use known
REL:ID directly.
Avoidance
- Never mismatch the exported variable name and the
nameproperty -- they must be identical. - Never use unsupported column types -- only use types listed in the Column Types section.
- Never mix basic and advanced relationship fields in the same record.
- Never omit the wrapper in
query_withscripts -- must use(function refineQuery(current, parent) { ... })(current, parent);format. - Never hardcode sys_id strings in record references -- use
${record.$id}. - Never omit
allowWebServiceAccesswhen the table will be accessed via REST -- it defaults tofalse, and the Table API will return 403 even with correct ACLs. - Never omit the ownership prefix when you don't own the table -- a new table's
namemust 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. - 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
| Data | Column Type | Notes |
|---|---|---|
| Short text | StringColumn | maxLength < 254 renders single-line |
| Long text | MultiLineTextColumn or StringColumn | maxLength >= 255 renders multi-line |
| Dropdown/choices | ChoiceColumn or StringColumn with choices + dropdown | StringColumn allows free-text too |
| True/false | BooleanColumn | |
| Whole numbers | IntegerColumn | Supports min and max |
| Decimal numbers | DecimalColumn | |
| Floating point | FloatColumn | Use scale for decimal places |
| Foreign key | ReferenceColumn | Set referenceTable |
| Multi-value reference | ListColumn | Set referenceTable |
| Multi-select picker | SlushBucketColumn | Set script for available items |
| Date only | DateColumn | Format: yyyy-mm-dd |
| Date and time | DateTimeColumn | Format: yyyy-mm-dd HH:mm:ss |
| Duration | DurationColumn | Default: { days, hours, minutes, seconds } |
| Time of day | TimeColumn | Default: { hours, minutes, seconds } |
| HTML/rich text | HtmlColumn | Use html() helper for defaults |
| URL | UrlColumn | Must be http:// or https:// |
EmailColumn | Must be user@domain format | |
| JSON | JsonColumn | Use json() helper for defaults |
| Field list | FieldListColumn | Needs dependent or attributes.table |
| Server script | ScriptColumn | |
| Radio buttons | RadioColumn | Requires 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
| Name | Type | Description |
|---|---|---|
label | String or Array | Display label for the column. |
maxLength | Number | Maximum value length. Default: 40. |
active | Boolean | Display in lists and forms. Default: true. |
mandatory | Boolean | Require a value to save. Default: false. |
readOnly | Boolean | Prevent editing. Default: false. |
default | Any | Default value when creating a record. |
choices | Object | Choices for ChoiceColumn and compatible types. |
dropdown | String | 'none', 'dropdown_without_none', 'dropdown_with_none', 'suggestion'. |
attributes | Object | Dictionary attributes. |
dynamic_value_definitions | Object | Dynamic defaults: calculated_value, dynamic_default, dependent_field, choices_from_other_table. |
function_definition | String | Glide 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 withreferenceQualholding an encoded query.'advanced'— pair withreferenceQualholding ajavascript:scripted qualifier.'dynamic'— pair withdynamicRefQualpointing at asys_filter_option_dynamicrecord.
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 theReferenceVariableandListCollectorVariabletopics. For an inherited column on an extended table, useOverrideColumnand itsreferenceQualifierproperty instead — see theOverrideColumntopic 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' }),
},
})
Relationship and Related List Reference
Determine the Relationship Path
- Reference field exists between tables: Implicit relationship (no
sys_relationshipneeded). - No reference field or custom query: Explicit relationship (create
sys_relationshiprecord). - 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)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Descriptive name |
basic_apply_to | TableName | No | Parent table (basic mode) |
basic_query_from | TableName | No | Child table (basic mode) |
reference_field | FieldName | No | Reference field for the relationship |
query_with | Script | No | Script to refine the query |
advanced | Boolean | No | Whether advanced mode (default: false) |
simple_reference | Boolean | No | Whether simple reference relationship |
Multiple Related Lists Pattern
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.