Core concepts
This page explains how the SDK thinks — the handful of rules behind every request and response. The Getting Started guide gets you to a first successful call by imitation; after this page, you should be able to predict what any method returns without checking the docs.
The five rules:
- You build URLs by chaining methods. Nothing is sent until you call a verb.
- Every verb takes a
context.Contextand a per-verb configuration.nilconfiguration is always valid. - Responses are envelopes. Your data is behind
GetResult(). - Models are backed, not plain structs. Getters return
(value, error), and values come back as pointers. - Errors are typed. API failures arrive as
*core.ServiceNowError, SDK misuse as sentinel errors.
1. Builders and chaining
The client is the root of a tree of request builders. Each chained call
clones the parent's URL parameters and narrows the path; the request only
executes when you call a verb method (Get, Post, Put, Delete):
// Each call narrows the URL. Nothing is sent until a verb method runs.
//
// client.Now() → /api/now
// .Table("incident") → /api/now/table/incident
// .ByID("{SysID}") → /api/now/table/incident/{sys_id}
builder := client.Now().Table("incident").ByID("{SysID}")
// The verb method (Get/Post/Put/Delete) executes the request.
response, err := builder.Get(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
Because intermediate builders are just values, you can hold onto one and reuse
it — client.Now().Table("incident") is a valid thing to store in a variable
and call Get on twice.
Fluent vs. standard
Everything above is the fluent modality — the recommended path. Every reference page also shows a standard modality, where you construct the request builder yourself from a raw URL:
// Standard modality: construct the request builder yourself from a raw URL.
rawURL := "https://{instance}.service-now.com/api/now/v1/table/{TableName}"
requestBuilder := tableapi.NewDefaultTableItemRequestBuilder(rawURL, client.GetRequestAdapter())
stdRecord, err := requestBuilder.Get(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
Use standard only when the URL is dynamic (for example, following a link returned by another API) or the resource isn't reachable through the fluent chain. The two modalities hit identical endpoints and return identical types — they differ only in who builds the URL.
2. Context and request configuration
Every verb method has the same shape: a context.Context first, an optional
per-verb RequestConfiguration last (plus a body parameter for Post/Put).
Deadlines and cancellation on the context propagate to the HTTP call:
// Every verb method takes a context.Context; deadlines and cancellation
// propagate to the underlying HTTP call.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
response, err := client.Now().Table("{TableName}").Get(ctx, nil)
if err != nil {
log.Fatal(err)
}
Each verb on each resource has its own configuration and query-parameter
types — TableRequestBuilderGetRequestConfiguration isn't interchangeable
with the Post variant. This is deliberate: the type tells you exactly which
parameters that verb supports, so an option that doesn't exist can't compile.
// Every verb has its own RequestConfiguration and QueryParameters types.
// Pass nil when you have nothing to configure.
query := "active=true^priority=1"
limit := int32(10)
config := &tableapi.TableRequestBuilderGetRequestConfiguration{
QueryParameters: &tableapi.TableRequestBuilderGetQueryParameters{
Query: &query,
Limit: &limit,
},
}
response, err := client.Now().Table("{TableName}").Get(context.Background(), config)
if err != nil {
log.Fatal(err)
}
Passing nil instead of a configuration is always valid and means "no
options."
3. Response envelopes
Verb methods don't return your record directly — they return an envelope
mirroring ServiceNow's REST responses, which wrap everything in a result
key. Collection endpoints return a collection envelope (GetResult() →
slice); item endpoints return an item envelope (GetResult() → one model):
// Collection endpoints return a collection envelope…
listResponse, err := client.Now().Table("{TableName}").Get(ctx, nil)
if err != nil {
log.Fatal(err)
}
// …and the records live behind GetResult().
records, err := listResponse.GetResult()
if err != nil {
log.Fatal(err)
}
fmt.Printf("got %d records\n", len(records))
// Item endpoints work the same way, with a single record inside.
itemResponse, err := client.Now().Table("{TableName}").ByID("{SysID}").Get(ctx, nil)
if err != nil {
log.Fatal(err)
}
record, err := itemResponse.GetResult()
if err != nil {
log.Fatal(err)
}
If you're wondering "why does everything need GetResult()?" — that's the
envelope. The envelope is also where response-level extras live, such as the
pagination links used by the page iterator.
4. Backed models and the pointer-getter pattern
Models aren't plain structs. Each model wraps a backing store — a key/value map tracking which fields the instance actually sent and which you changed. That design is why the accessors look the way they do:
- Getters return
(value, error)— the store distinguishes "field absent" from "field empty," and reports it instead of silently zeroing. - Values come back as pointers —
nilmeans the instance sent no value, which is different from""or0.
Reading a field from a table record is therefore a three-step unwrap:
// 1. Get the field. A missing field is an error, not a zero value.
element, err := record.Get("number")
if err != nil {
log.Fatal(err)
}
// 2. Unwrap the element. Fields carry a value, a display value, and a link.
value, err := element.GetValue()
if err != nil {
log.Fatal(err)
}
// 3. Convert to the type you expect. The result is a pointer: nil means
// the instance sent no value, distinct from "" or 0.
number, err := value.GetStringValue()
if err != nil {
log.Fatal(err)
}
if number != nil {
fmt.Printf("number: %s\n", *number)
}
The middle step exists because a ServiceNow field is more than a value: a
RecordElement carries the raw value (GetValue()), the human-readable
display value (GetDisplayValue()), and, for reference fields, a link to the
referenced record (GetLink()).
Writing goes through setters for the same reason — the store records the change so only fields you set are serialized into the request:
newRecord := tableapi.NewTableRecord()
if err := newRecord.SetValue("short_description", "created by servicenow-sdk-go"); err != nil {
log.Fatal(err)
}
if err := newRecord.SetValue("priority", "1"); err != nil {
log.Fatal(err)
}
5. Typed errors
Two kinds of errors come out of a verb method:
- API errors — the instance answered with a failure status. These are
*core.ServiceNowErrorvalues carrying the instance's own error message and detail; unwrap them witherrors.As. - Sentinel errors — the SDK refused to send at all (nil client, nil
request adapter, nil body). These are shared sentinels like
snerrors.ErrNilRequestBuilder; match them witherrors.Is.
The Error handling guide covers both in detail, with the full status-code mapping.
Where to go next
- Table operations — the concepts above, applied to real CRUD tasks.
- Query builder — compose
sysparm_querystrings from typed conditions instead of writing encoded queries by hand. - Pagination — iterate past the first page of a collection response.
- API Reference — every module, every verb, in the fluent and standard modalities.