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 |
name-list | List all records (with optional query) | |
name-get | Get one record by ID | |
name-create | Create a new record | |
name-update | Update an existing record | |
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:
- Iterate directly with for:
(for ([cust (customers-list)]) (displayln (hash-ref cust 'displayName))) - Convert to a list with bc-result->list:
(define all-customers (bc-result->list (customers-list))) (length all-customers) - Access by index with bc-result-ref:
(bc-result-ref (customers-list) 0) ;; first customer
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:
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—
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:
exn:fail:bcnav:auth? —
Authentication problems (invalid credentials, expired token) exn:fail:bcnav:http? —
API errors (record not found, validation failures, conflicts) exn:fail:bcnav:rate-limit? —
Too many requests (bcnav retries automatically)
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.