On this page:
5.1 The entity pattern
5.2 Listing records
5.2.1 Using queries
5.2.2 Automatic pagination
5.3 Getting a single record
5.4 Creating records
5.4.1 Discovering available fields
5.4.2 Working with the returned record
5.5 Updating records
5.5.1 Understanding ETags
5.5.2 Update return value
5.6 Deleting records
5.6.1 ETags for deletes
5.7 Handling errors
5.8 Working with entity modules
5.9 Example:   Customer management script
5.10 Next steps
9.3

5 Working with Entities🔗

This tutorial covers the full lifecycle of working with Business Central entities: listing, reading, creating, updating, and deleting records. Every standard BC entity follows the same patterns, so once you learn how to work with customers, you’ll know how to work with vendors, items, sales orders, and everything else.

5.1 The entity pattern🔗

Every BC entity in bcnav follows a consistent naming convention:

Function

Pattern

Purpose

customers-list

name-list

List all records (with optional query)

customers-get

name-get

Get one record by ID

customers-create

name-create

Create a new record

customers-update

name-update

Update an existing record

customers-delete

name-delete

Delete a record

The same pattern applies to all entities: vendors-list/vendors-get, items-list/items-get, sales-order-list/sales-order-get, and so on.

Each entity module also exports a bc-entity struct value (e.g., customers) that you can use with inspect, peek, and sample to explore the schema and preview data.

5.2 Listing records🔗

The -list function returns a bc-result containing all matching records:

(require bcnav/api/master-data)
 
(customers-list)

In the REPL, you’ll see a compact summary:

#<bc-result: customers (42 records, ~125 KB)>

The bc-result wrapper provides safe REPL printing (won’t hang on large datasets) and O(1) access to count and elements. To work with the records, you can:

Each record is a hash table with symbols as keys. The special 'odata.etag key contains the record’s ETag, which you’ll need for updates (explained below).

;; Example record structure
#hasheq((id . "01234567-89ab-cdef-0123-456789abcdef")
        (number . "C00010")
        (displayName . "Contoso Ltd")
        (type . "Company")
        (email . "info@contoso.com")
        (phoneNumber . "555-0100")
        (blocked . #f)
        (odata.etag . "W/\"abc123...\"")
        (... . "…"))

5.2.1 Using queries🔗

Pass a query to filter, select fields, or control results:

;; Get unblocked customers with specific fields
(customers-list #:query (make-query
                          #:filter (eq 'blocked #f)
                          #:select '(id displayName email)
                          #:orderby 'displayName
                          #:top 100))

See Querying with OData for complete query documentation.

5.2.2 Automatic pagination🔗

BC’s API returns results in pages (typically 100 records per page). By default, bcnav automatically fetches all pages and combines them into a single list. You can disable this:

;; Get only the first page
(parameterize ([current-bc-auto-paginate #f])
  (customers-list))

When auto-pagination is disabled, you’ll need to handle @odata.nextLink yourself if you want additional pages.

5.3 Getting a single record🔗

Use the -get function with a record ID:

(customers-get "01234567-89ab-cdef-0123-456789abcdef")

This returns a single hash table:

#hasheq((id . "01234567-89ab-cdef-0123-456789abcdef")
        (number . "C00010")
        (displayName . "Contoso Ltd")
        (type . "Company")
        (email . "info@contoso.com")
        (... . "…"))

The ID must be the full GUID, not the human-readable number (like C00010).

5.4 Creating records🔗

Use the -create function with a hash table of field values:

(customers-create
  (hasheq 'displayName "New Customer Inc"
          'type "Company"
          'email "hello@newcustomer.com"
          'phoneNumber "555-0199"))

The function returns the created record, including server-generated fields like 'id, 'number, and 'odata.etag.

5.4.1 Discovering available fields🔗

Different entities have different fields. Use inspect to see what fields are available and their types:

(inspect customers)

This displays a formatted table of all fields, their types, and nullability.

5.4.2 Working with the returned record🔗

Since create returns the new record, you can immediately use it:

(define new-customer
  (customers-create
    (hasheq 'displayName "Acme Corporation"
            'email "orders@acme.com")))
 
(printf "Created customer ~a with ID ~a\n"
        (hash-ref new-customer 'displayName)
        (hash-ref new-customer 'id))

5.5 Updating records🔗

Use the -update function with a record ID and a hash of fields to change:

(customers-update "01234567-89ab-cdef-0123-456789abcdef"
                  (hasheq 'email "newemail@contoso.com"
                          'phoneNumber "555-0200"))

You only need to include the fields you want to change—other fields are left alone.

5.5.1 Understanding ETags🔗

BC uses ETags (entity tags) for optimistic concurrency control. An ETag is like a version number for a record. When you update a record, BC checks that your ETag matches the current one. If someone else modified the record since you read it, the ETags won’t match and the update fails.

By default, bcnav sends a wildcard ETag (If-Match: *) that tells BC "update regardless of the current version." This is convenient but can overwrite someone else’s changes.

For safer updates, read the record first and pass its ETag:

;; Read the current record
(define cust (customers-get "01234567-..."))
 
;; Make the update with the current ETag
(customers-update
  (hash-ref cust 'id)
  (hasheq 'email "updated@contoso.com")
  #:etag (hash-ref cust 'odata.etag))

If someone modified the record between your read and update, you’ll get a 412 Precondition Failed error. You can then re-read the record, merge changes if needed, and try again.

5.5.2 Update return value🔗

Like create, update returns the modified record with its new ETag:

(define updated
  (customers-update "01234567-..."
                    (hasheq 'email "new@example.com")))
 
;; The new ETag for future updates
(hash-ref updated 'odata.etag)

5.6 Deleting records🔗

Use the -delete function with a record ID:

(customers-delete "01234567-89ab-cdef-0123-456789abcdef")

Delete returns (void) on success. If the record doesn’t exist or can’t be deleted (due to referential constraints), you’ll get an error.

5.6.1 ETags for deletes🔗

Like updates, you can pass an ETag to ensure you’re deleting the expected version:

(define cust (customers-get "01234567-..."))
(customers-delete (hash-ref cust 'id)
                  #:etag (hash-ref cust 'odata.etag))

5.7 Handling errors🔗

API operations can fail for various reasons. bcnav raises specific exception types:

Use with-handlers to catch and handle errors:

(with-handlers
    ([exn:fail:bcnav:http?
      (lambda (e)
        (printf "API error: ~a\n" (exn-message e))
        #f)])
  (customers-get "nonexistent-id"))

The exn:fail:bcnav:http exception includes details about what went wrong:

(with-handlers
    ([exn:fail:bcnav:http?
      (lambda (e)
        (printf "Status: ~a\n" (exn:fail:bcnav:http-status-code e))
        (printf "Body: ~a\n" (exn:fail:bcnav:http-body e)))])
  (customers-create (hasheq)))  ;; Missing required fields

5.8 Working with entity modules🔗

bcnav organizes entities into modules by category:

Module

Entities

bcnav/api/master-data

customers, vendors, items

bcnav/api/sales

sales-order, sales-invoice

bcnav/api/purchasing

purchase-order, purchase-invoice

bcnav/api/finance

account, journal, general-ledger-entry

Import the modules you need:

(require bcnav
         bcnav/api/master-data
         bcnav/api/sales)

5.9 Example: Customer management script🔗

Here’s a complete example that finds customers without email addresses and updates them:

;; #lang racket
 
(require bcnav
         bcnav/api/master-data)
 
;; Configuration (assume environment variables are set)
(current-bc-tenant (getenv "BC_TENANT_ID"))
(current-bc-client-id (getenv "BC_CLIENT_ID"))
(current-bc-client-secret (getenv "BC_CLIENT_SECRET"))
(current-bc-company (getenv "BC_COMPANY_ID"))
 
(bc-authenticate!)
 
;; Find customers with empty email
(define customers-without-email
  (customers-list #:query (make-query
                            #:filter (eq 'email "")
                            #:select '(id displayName email))))
 
(printf "Found ~a customers without email\n"
        (bc-result-count customers-without-email))
 
;; Update each one with a placeholder
;; bc-result supports direct iteration with for
(for ([cust customers-without-email])
  (define id (hash-ref cust 'id))
  (define name (hash-ref cust 'displayName))
 
  (printf "Updating ~a... " name)
 
  (with-handlers
      ([exn:fail:bcnav:http?
        (lambda (e)
          (printf "FAILED: ~a\n" (exn-message e)))])
    (customers-update id (hasheq 'email "pending@example.com"))
    (printf "done\n")))

5.10 Next steps🔗

Now you know how to work with entities. Continue to Metadata Discovery to learn how to explore the API schema and discover available fields using inspect and other tools.