Lesson 5 Trick: Jump ~25 minutes

Your Own Template

Build an endpoint and a nested data load from scratch, using data you extracted yourself

Everything so far has handed you a template. This lesson does not.

You have a low-stock report from Lesson 4. The warehouse needs replenishing, which means turning that flat table into purchase orders — a nested structure, one order per supplier, one line per product.

Flat data in, nested data out. That is the job, on every project, forever.

Step 1 — Look Before You Build

Never write a template against an API you haven’t looked at. Copy this, open API Catalogue, click New Endpoint from Clipboard, save, and click Run API:

// Name: Puppy School — List Purchase Orders
// Method: GET
// Path: /purchase-orders
// QueryParam: size: 1 [enabled]

The Console gives you two ways to look at what came back:

Where What you see
The Raw tab The whole run, transaction by transaction: request and response
The Completed tab: right-click a row → View transaction The transaction behind that one row, request and response side by side. Handy when a run has thousands

Try both. A GET returns a page: totalCount and the paging fields, with the orders inside the data array. A POST creates one order. You’ll send the one order, not the page around it.

Step 2 — Send One Back

Now the endpoint that becomes your template. Copy this and click New Endpoint from Clipboard again:

// Name: My Replenishment Orders
// Method: POST
// Path: /purchase-orders

It opens with an empty Request Body. Switch back to the Console, copy the one order out of the Raw tab, and come back. Click in the Request Body, press Ctrl+V, then click Format.

Hint: copy one order

In the Raw tab, click the arrow in the margin beside the { just under "data": [. The whole order folds into one line. Select that line and press Ctrl+C. Everything inside the fold comes with it.

Save, Run API. It fails:

duplicate key value violates unique constraint "playground_purchase_orders_pkey"

That order already exists. id is its primary key, and the first key in your body, just under the opening {. Put your cursor on the "id" line and press Ctrl+D to delete it. Without an id, the API creates a new order. Or send your own, as long as it’s a new UUID.

Key Why it goes
id The order’s primary key. Without it, the API creates a new order

Extra credit: see it land

Open History and click your Puppy School — List Purchase Orders run from Step 1. In the Raw tab, note the totalCount. Now click Re-run at the bottom. Same request, sent again. totalCount goes up by one. That one is yours.

Save, Run API. 201. The API has created a new purchase order and given it an id of its own. The endpoint works.

Step 3 — Turn It Into a Template

Stay on My Replenishment Orders. The order you just sent is still in its Request Body. You turn it into the template right there, one key at a time.

Keep one order line. The order has five lines, lineNumber 1 to 5, each in its own { … }. Put your cursor on the first {, line 16, just under "lines": [:

  "lines": [
    {                          // line 16: cursor here
      "sku": "SKU-WOOF-004",
      …

Press Ctrl+D four times. Each press takes a whole order line, { to },, and the next moves up into its place. The last one is left, with no comma to tidy.

One line in, hundreds out

The line you kept isn’t one order line any more. It’s the pattern for all of them. When you run a batch, Dobermann sorts your rows into orders (the header decides which; more on that in Step 6), then stamps out a copy of this line for each row in the order. Three rows, three lines. A hundred and eighty rows, a hundred and eighty lines. Your data stays a flat table, and the API still gets properly nested orders. That’s why the other four could go: a template only ever needs one.

Dobermann repeats that one line for every row of your data.

Keep only what the API needs. The GET brought back more than a POST needs: fields the API works out for itself. Who owns the order, when it arrived, the names behind the GLNs, the product details behind the gtin, the totals. The API fills those in on every new order, so you don’t send them. You’ll see them filled in on your orders in Step 7.

Keep these. Put your cursor on every other key and press Ctrl+D.

In the header:

Keep What it is
poNumber The order number. You’ll generate it below
buyerGln Who’s buying: the warehouse’s GLN
supplierGln Who’s supplying: the supplier’s GLN
orderDate When it was ordered. Keep it, but commented out (below)
requestedDeliveryDate When it should arrive. Leave the value as it is
status draft: not sent to the supplier yet. Leave it
currency USD. Leave it
lines The order line you kept

In the order line:

Keep What it is
gtin The product’s unique identifier. Required: the API refuses a line without one
orderedQty How many to order
uom The unit: EA, CS or PL
supplierSku The supplier’s own part number. Optional: not every supplier has them
lineNumber The line’s place in the order

Comment out orderDate. Put your cursor on it and press Ctrl+/. A commented line isn’t sent, so the API dates each order today. The line stays in the template, ready to become a generated date later.

Make them variables. Put your cursor on buyerGln, supplierGln, gtin, orderedQty and uom in turn and press Ctrl+M on each. Dobermann turns each value into a typed {{variable}} and keeps the original as a comment.

supplierSku is null, and Ctrl+M can’t tell what type a null should be. Put your cursor on it and use the toolbar instead: Line Variable → Input. That makes it a string.

Three edits by hand:

  • poNumber: press Ctrl+M three times to reach {{A8:}}, pick sequence, and type a prefix in front. Dobermann numbers each order at run time.
  • lineNumber: the same, with sequence:local. It restarts at 1 in each order.
  • supplierSku: it’s optional, so click inside the variable, open Modifier, choose opt. A blank cell then leaves the key out instead of sending "".

Your Request Body is now:

{
  "poNumber": "PO-REPLEN-{{A8:sequence}}",
  "buyerGln": "{{buyerGln:string}}", //5012345000039
  "supplierGln": "{{supplierGln:string}}", //4012345000054
  // "orderDate": "2026-02-04T08:00:00+00:00",
  "requestedDeliveryDate": "2026-02-18",
  "status": "draft",
  "currency": "USD",
  "lines": [
    {
      "uom": "{{uom:string}}", //PL
      "gtin": "{{gtin:string}}", //00012345600081
      "lineNumber": "{{A8:sequence:local}}",
      "orderedQty": "{{orderedQty:number}}", //404
      "supplierSku": "{{supplierSku:string|opt}}" //null
    }
  ]
}

Your comments show whichever order you copied.

Save. The footer has grown a Run Batch button beside Run API. You built that.

Step 4 — One Order, One Line

Same rule as Lesson 2: one record before a thousand.

Click Run API. The form has six fields — one per variable, and nothing for the two A8 values or the three you left hardcoded. Copy this, then click Paste:

supplierGln,buyerGln,gtin,supplierSku,orderedQty,uom
4012345000016,0614141000012,00012345600012,CTM-0001,200,EA

Check the fields, then click Run.

One purchase order, with one line. And one line is all Run API will ever give you: the form holds a single row, so it can fill the lines array exactly once. That’s not a flaw to work around — it’s what the button is for. Run API proves the template. Run Batch does the work.

Look at the response in the Completed tab: your generated PO number, "lines": 1, and supplierName and buyerName, filled in by the API from the two GLNs you sent.

Step 5 — One Order, Many Lines

Chew Toy Manufacturing has its order, with one line. Next, one order with many.

The lines are already sitting in Dobermann: you fetched them in Lesson 4. Open History and open your Puppy School — Inventory Report run. Nothing is re-fetched; the Console reopens with every row you pulled. On the Completed tab, make sure the view button shows Low Stock by Location, then search:

low_stock +Golden +"Wagmore Components Ltd"

Low stock, at this warehouse, from one supplier. About 200 rows.

In Lesson 4 you copied rows out. This time you chain them: click Chain, beside Copy and Export. It sends the rows on screen, in the view’s columns, straight into another endpoint’s Run Batch. No clipboard, no file.

Suggested offers Puppy School — Bulk Inventory Upload. Every one of its variables has a column here, so the rows would fit, but you’d be reloading stock. A fit isn’t a purpose. In Send them to endpoint:, type Replenishment, select My Replenishment Orders and click Chain.

Run Batch opens with the rows already loaded, Chained from your report run, and stops on Map. Two match by name and map themselves — gtin, uom. The other four came out of a nested response with dotted names, so point each one at its column:

Variable Column
supplierGln product.supplier.gln
buyerGln location.gln
supplierSku product.supplierSku
orderedQty product.reorderQty

supplierSku arrives as a guess, sku. That’s the product’s own code, not the supplier’s, so change it. The columns you don’t use — status, quantityOnHand and the rest — are simply ignored. No gtin or location.gln column? Your view is older than this lesson: tick them in the view (Lesson 4, Step 3) and chain again.

Click Next, and Dobermann remembers this mapping: the next batch on this endpoint maps these columns for you. Leave Update Endpoint Template unticked: it would rename your variables after the columns, and buyerGln says what the field means to the API.

You’re on Validate, and every row passes. Click Next again, to Review.

  • 1 API call. About two hundred rows, one request.
  • It’s one purchase order: the header once, and your one line repeated for every row, lineNumber counting up from 1.

Every row has the same buyer and the same supplier, so every row belongs to the same order. That’s the one line from Step 3 at work.

Click Next, and Execute. One request, one order, a couple of hundred lines.

Step 6 — Many Orders, One Chain

How Dobermann decides where one order ends and the next begins. Everything above lines is the order’s header, and a header is a statement about every line beneath it. So Dobermann starts a new order whenever any header field changes — here, a different supplier or a different buyer. Rows that agree on all of them are one order; their lines stack up underneath. That’s Step 5: every row agreed, so you got one order. The generated PO number isn’t part of that. It’s handed out after the rows are grouped, one per order.

The consequence is worth holding on to: a header field should only ever hold something that’s true of the whole order. Put a per-line value up there by mistake — a quantity, a gtin — and you’ll get one order per row. That’s Dobermann being right and the template being wrong.

Three suppliers to go, in one batch. Back in the report’s Console (the Chained from link takes you there), search:

low_stock +Golden -"Chew Toy Manufacturing Inc" -"Wagmore Components Ltd"

Everyone except the two suppliers you’ve already ordered from. Around 540 rows, three suppliers. Click Chain.

My Replenishment Orders is under Suggested now: it remembered Step 5’s mapping. Select it and click Chain. Every column is mapped and every row is valid, so Run Batch walks through Map and Validate by itself and lands on Review:

  • 3 API calls. Five hundred-odd rows, three requests — one per supplier. Click the ⓘ beside API calls to see why: your rows grouped by buyerGln and supplierGln, one request per combination.
  • Each request is a single purchase order with well over a hundred lines, lineNumber starting again at 1 in each.
  • Your rows weren’t sorted by supplier. Dobermann sorted them before it folded them.
  • Find the order with supplierGln 4012345000054, K9 Precision Parts Corp. Its lines have no supplierSku key at all. Find 4012345000030, Boop & Snoot Supply Co. Every line has one. That’s opt.

A flat table went in, and Dobermann rebuilt the nesting from it. That is the trick the whole course has been walking towards. Nested APIs, flat source data, no scripting.

Click Next, and Execute. Three requests. Boom.

Step 7 — Prove It Landed

Go back to Puppy School — List Purchase Orders, set size to 6, and run it again.

Your orders are there, newest first: the three from Step 6 and the one from Step 5 with their lines nested underneath, the one-line order from Run API, and the copy you sent back in Step 2. Every line has its sku, description and unitPrice, filled in by the API from its gtin. Build a view over it if you want to see them properly — you know how now.

Now open your Step 6 batch from History. Its Console has a Links tab: Came From names your Inventory Report run, the view, and how many rows it sent. Open the report run and its Links tab lists both batches under Sent To. When someone asks where an order’s lines came from, that’s the answer.


That’s Puppy School. You can connect to an API, load data at volume, deal with the failures, get data back out in a shape a human can use, and build your own templates for structures nobody handed you.

The rest is just other people’s APIs.

🐾 Dobermann Philosophy

Every tool can do the demo. The test is whether it can do the thing nobody demoed — an endpoint you’ve never seen, a structure invented by a committee, a spreadsheet from a system that was decommissioned in 2011. Dobermann is built so the answer is always the same handful of moves: look at the shape, write one, make it a template, load the data.

🦴 Dig Deeper

Flat-to-nested is the oldest problem in data integration and the reason most projects end up with a folder of one-off scripts. The general shape is always the same: decide which column identifies the parent, group the rows by it, and nest the rest. Template Variables and Batch Preparation cover everything Dobermann can do with it.

Check My Homework

To pass: Five or more purchase orders of your own, across four or more suppliers — at least one with a single line and at least one with many