Skip to main content
Version: v2.0

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:

  1. You build URLs by chaining methods. Nothing is sent until you call a verb.
  2. Every verb takes a context.Context and a per-verb configuration. nil configuration is always valid.
  3. Responses are envelopes. Your data is behind GetResult().
  4. Models are backed, not plain structs. Getters return (value, error), and values come back as pointers.
  5. 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 pointersnil means the instance sent no value, which is different from "" or 0.

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.ServiceNowError values carrying the instance's own error message and detail; unwrap them with errors.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 with errors.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_query strings 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.
Was this page helpful?