Skip to main content
Version: main

Conventions reference

This is the dense page: every convention reviewers hold a PR to, in one place, organized for lookup rather than reading order. If you're new, the first PR walkthrough and architecture pages teach these gradually — come back here when you want the whole contract.

Each convention exists for a reason; where the reason isn't obvious, the link goes to the design decision behind it.

Module anatomy

One package per ServiceNow API surface (tableapi/, attachmentapi/, …), all structurally identical — the uniformity is the hand-written substitute for generated code (why?).

RuleDetail
File per concern<resource>_request_builder.go holds the builder; each verb's query parameters and request configuration live in their own files (..._<verb>_query_parameters.go, ..._<verb>_request_configuration.go). Never inline them.
Constructor triadExactly three constructors per builder: New<X>RequestBuilderInternal(pathParameters, adapter, …), NewDefault<X>RequestBuilder(rawURL, adapter), New<X>RequestBuilder(rawURL, adapter, factory).
Verb methodsOne method per HTTP verb (Get/Post/Patch/Put/Delete), each with a matching To<Verb>RequestInformation.
URL templatesUnexported consts in Kiota URI-template syntax: {+baseurl}/api/now/...{?sysparm_query,sysparm_limit}.
GenericsBuilders and responses are constrained by model.ServiceNowItem (store.BackedModel + serialization.Parsable + GetSysID()).
ChainingEach child-builder method clones the parent's path parameters (maps.Clone), adds its own, and passes the same RequestAdapter.
Package readmeEvery module carries a Readme.md summarizing the endpoint, mirroring tableapi/Readme.md.

Reference implementations: tableapi/ is the canonical, fullest example (every verb, paging, generics); policyapi/ is the minimal one. When in doubt, look there — and if msgraph-sdk-go does it a certain way, that's the default answer here too.

The nil-guard prologue

Every verb method opens with the same two guards, returning shared sentinels — never a fresh errors.New:

if conversion.IsNil(rB) || conversion.IsNil(rB.RequestBuilder) {
return nil, snerrors.ErrNilRequestBuilder
}
if conversion.IsNil(rB.GetRequestAdapter()) {
return nil, snerrors.ErrNilRequestAdapter
}

internal/conversion.IsNil is the standard nil check everywhere (it's reflect-safe for typed-nil interfaces); a bare x == nil on an interface is a review comment waiting to happen.

The error taxonomy

Sentinels live in three places that look similar but aren't interchangeable (why?):

LocationImportWhat belongs there
errors/errors.gosnerrorsThe shared cross-package sentinels (ErrNilRequestBuilder, ErrNilRequestAdapter, ErrNilResponse, ErrNilConfig, ErrNilBody, …). Check here first — almost every nil-guard uses one of these.
Root errors.goservicenowsdkgoA couple of client-configuration sentinels only.
<module>api/errors.gopackage-localConditions genuinely specific to that module.

Rules:

  • Reuse a sentinel by identity, not by matching text — duplicating the message as a fresh errors.New breaks errors.Is for callers, which is exactly the v1 bug the standardization fixed.
  • Message phrasing: "[parameter] cannot be nil" for nil checks, "[parameter] is required" for missing inputs; no contractions.
  • HTTP error mapping always goes through core.DefaultErrorMapping() — never a bespoke error struct. Per-module mappings register via internal.GetErrorRegistryInstance().

Model rules

Models are backing-store-backed, not plain structs (why?):

  • Embed core.BaseModel; never add plain data fields to a model.
  • Every property is a GetX() (T, error) / setX(T) error pair built on internal/store (DefaultBackedModelAccessorFunc / DefaultBackedModelMutatorFunc).
  • Serialize() / GetFieldDeserializers() are built from the internal/serialization generators — no hand-rolled property plumbing.

Compose internal/, don't reinvent

If you're writing a nil check, a header string, an accessor, or serializer plumbing inside a module, stop — the helper almost certainly exists:

PackageUse it for
internal/conversionIsNil, As2, collection casts, string→primitive converters
internal/storeBacking-store accessor/mutator generators
internal/serializationSerialize/SerializeXFunc/DeserializeXFunc generators
internal/httpRequestHeader/HTTPHeader/ContentType enums, default middleware/client
internal/ast + query/The fluent query-condition builder and its sysparm_query renderer

internal/ is never imported by consumers — it's implementation surface, so helpers can evolve freely.

Testing conventions

The full treatment is the testing guide; the reviewable contract in brief:

  • Every exported type and method ships with tests, in the same PR — co-located _test.go, table-driven with testify, HTTP stubbed with httpmock (plus internal/mocking doubles).
  • Tables include the failure rows: nil-guard sentinels, at least one mapped API error, and the success path.
  • Test data looks like ServiceNow (INC0010001, not "foo"), and the request is asserted (headers, query, body), not just the response.
  • Bug fixes carry the test that fails before the fix.

Git, commits, and releases

  • Branches: type/kebab-description off mainfix/tableapi-nil-pointer, docs/error-sentinel-notes.
  • Commits and PR titles: Conventional Commits (feat(scope): …, fix: …, BREAKING CHANGE: footer for majors). CI lints the PR title; it becomes the squash commit.
  • CI/workflow changes are always chore — anything touching .github/workflows/, scripts/ CI helpers, or other pipeline plumbing is chore:, never fix:/feat:, even if it fixes a broken run. These changes don't affect the published SDK, so they must not surface in CHANGELOG.md as a fix or feature.
  • Never edit VERSION or CHANGELOG.mdrelease-please generates both from commit messages.
  • Local gate before review: gofmt -s -w ., golangci-lint run ./..., go test ./... (config: .golangci.yml; just build|lint|fmt wrap the same commands).

Issue triage and labels

Issues use the following label taxonomy:

Priority (score = Impact + Risk-of-delay):

LabelScoreDescription
priority: urgent6Blocks a release or active work
priority: high5Should be picked up soon
priority: medium4Worth doing, no rush
priority: low2-3Nice to have, no urgency

Type labels: type: bug, type: feature, type: refactor, type: documentation, type: devops, type: epic, type: test

Module labels: Applied automatically by PR based on changed files (for example, module: table-api, module: core).

Status (workflow state, synced automatically):

LabelMeaning
status: newNewly filed, not yet triaged
status: reviewedTriaged, ready for work
status: in progressActively being worked on (set automatically when a PR is linked)
status: blockedBlocked on another issue, decision, or external dependency

Automation: When you open a PR with Closes #N in the description, the linked issue automatically moves to status: in progress. When the PR is closed without merge, it reverts to status: reviewed. Merged PRs close issues via GitHub's native behavior.

For the full triage workflow, scoring rubric, and examples, see Issue triage.

Documentation conventions

  • A PR that changes exported API surface updates the docs site (website/) in the same PR — or says why not in the description.
  • Go samples are single-sourced from website/snippets/*.go behind // [START x] / // [END x] region markers and rendered with the GoSnippet/GoExample components. CI compiles every snippet (go vet -tags snippets ./website/snippets/), so samples can't rot.
  • New pages register in website/sidebars.ts; preview with just serve-docs (Node 20+, just setup-docs first).
  • Design changes get an ADR plus a summary page here — see Why it's built this way. New modules get a blueprint first — see the documents API blueprint for the format.

The playbook, when you need it

Adding a whole new API surface pulls all of the above together in order: Add a new API module.

Was this page helpful?