What you will build
You will drive one Grid model through its complete public API lifecycle: prepare and deploy exact source in an editable authoring phase, inspect its identity, resolve an output, change a declared input, replace source with compare-and-swap protection, diagnose a stale write, lock the finished source with protected mode, rename the model, and delete it.
This tutorial creates temporary server state. It does not publish a runtime execution receipt; every live checkpoint below is an expected result to confirm against your own Grid build.
Before you begin
You need:
- a running Grid Server Build with both Node and Runtime Host ready;
curlandjqin a terminal;- a public API bearer token from your operator;
- the two source assets above, saved as
revenue-lifecycle.gridandrevenue-lifecycle-v2.grid; - the checked response projection saved as
create-response.fixture.json.
Set these shell variables without committing their values:
export GRID_URL="http://localhost:3000"
export GRID_TOKEN="replace-with-your-api-token"
All requests in this build go to the Node API, normally port 3000. Node owns HTTP, authentication, and the control plane; the private Runtime Host owns execution and resident model state. Never point these commands at Runtime Host or use its RPC credential. No public Server Build installation coordinate is documented, so obtain the running build through your deployment operator.
Read API conventions and authentication before adapting the commands for another environment.
1. Prove the local request body before sending it
Build the deploy request from the exact downloaded source rather than escaping a multiline program by hand:
jq -n --rawfile source revenue-lifecycle.grid '{
id: "revenue-lifecycle",
protectedMode: false,
source: $source
}' > /tmp/revenue-lifecycle-deploy.json
jq '{id, protectedMode, sourceEndsWithModel: (.source | endswith("END MODEL\n"))}' \
/tmp/revenue-lifecycle-deploy.json
Deterministic local checkpoint:
{
"id": "revenue-lifecycle",
"protectedMode": false,
"sourceEndsWithModel": true
}
This checkpoint validates only your local request shape. It is not proof that Grid compiled or ran the model.
Check the representative create-response projection before making a network request:
jq -e '
.id == "revenue-lifecycle" and
.protectedMode == false and
.sourceHash == "29541dbca667095e" and
(.declaredInputs == ["Revenue", "TaxRate"]) and
(.declaredOutputs == ["NetRevenue"]) and
(.diagnostics == [])
' create-response.fixture.json
Expected checkpoint: true. The fixture is a deterministic projection of the response fields this tutorial consumes, tied to the exact baseline source digest. It is not a captured live response or a runtime receipt.
2. Check readiness and deploy
Readiness does not require the public API token:
curl --fail-with-body "$GRID_URL/readyz" | jq '{status, probes}'
Continue only after the response is 200 with status: "ok". Then deploy:
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$GRID_URL/api/models" \
--data-binary @/tmp/revenue-lifecycle-deploy.json \
| tee /tmp/revenue-lifecycle-created.json \
| jq -e 'select(
.id == "revenue-lifecycle" and
.protectedMode == false and
.sourceHash == "29541dbca667095e" and
((.declaredInputs // []) | index("Revenue")) != null and
((.declaredInputs // []) | index("TaxRate")) != null and
((.declaredOutputs // []) | index("NetRevenue")) != null and
((.diagnostics // []) | length) == 0 and
(.parseDiagnostic? == null)
) | {id, sourceHash, declaredInputs, declaredOutputs, diagnostics}'
Expected checkpoint on a compatible build: the ID is revenue-lifecycle, protected mode is off during the source-authoring phase, the declared inputs include Revenue and TaxRate, the declared outputs include NetRevenue, and diagnostics contain no compile failure. Preserve the returned lowercase sourceHash, which is a server-owned truncated SHA-256 identity in this release.
If deployment returns a parseDiagnostic, show its code, span, snippet, and raw compiler message. Do not collapse it into “bad request.” See source compilation troubleshooting.
Know the retry and idempotency boundary
This lifecycle has no universal idempotency header. Decide by route and preserve the guard that route actually owns:
| Operation in this build | Repeat policy after an ambiguous timeout |
|---|---|
| Readiness, source reads, summaries, and symbol resolution | Repeat with bounded backoff; these requests do not author model state. |
Input PUT |
Refetch the symbol first. Reuse only an unchanged intent with the current expectedRequestRev; a stale guard must conflict. |
Source PUT |
Refetch and reconcile first, then use the current baseSourceHash. Never drop the guard. |
Create/redeploy and rename POST |
No general idempotency key is documented. Inspect the model before deciding whether another mutation is needed. |
| Delete | Confirm whether the model is already absent before repeating; the desired final state may match even when the second response does not. |
Connector ingress has a separate idempotencyKey contract, but that field does not make these model-lifecycle mutations idempotent. Honor Retry-After on 429 or 503, add jitter, and cap every retry loop.
3. Read back the exact source and resolve the baseline
Fetch the server-owned source identity:
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
"$GRID_URL/api/models/revenue-lifecycle/source" \
| tee /tmp/revenue-lifecycle-source.json \
| jq '{sourceHash, sourceBytes: (.source | utf8bytelength)}'
Resolve both the input and output:
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$GRID_URL/api/models/revenue-lifecycle/symbols/resolve" \
--data '{"symbols":["Revenue","NetRevenue"]}' \
| tee /tmp/revenue-lifecycle-baseline.json \
| jq '{
revenue: .Revenue.value.value,
netRevenue: .NetRevenue.value.value,
status: .NetRevenue.status,
requestRev: .Revenue.requestRev,
resolvedRev: .NetRevenue.resolvedRev
}'
Expected live checkpoint: revenue = 100000, netRevenue = 79000, and the output status is ready. Revision numbers are server-owned; record them rather than expecting a particular integer.
4. Make one deliberate input change
Capture the current Revenue request revision, then guard the write with it:
export REVENUE_REV="$(jq -r '.Revenue.requestRev' /tmp/revenue-lifecycle-baseline.json)"
jq -n --argjson rev "$REVENUE_REV" '{
symbol: "Revenue",
value: 120000,
expectedRequestRev: $rev
}' > /tmp/revenue-write.json
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X PUT "$GRID_URL/api/models/revenue-lifecycle/input" \
--data-binary @/tmp/revenue-write.json \
| jq '{symbol, status, value, requestRev, resolvedRev, lastError}'
Resolve NetRevenue again using the command from step 3.
Expected checkpoint:
NetRevenue = 94,800. The declaredRevenueinput is the intended mutation seam; do not write derived outputs even while this authoring deployment remains unprotected.
5. Replace source with compare-and-swap
The second asset adds an explicit TaxDue output while preserving the model's behavior. Use the hash fetched in step 3 as the compare-and-swap guard:
export BASE_SOURCE_HASH="$(jq -r '.sourceHash' /tmp/revenue-lifecycle-source.json)"
jq -n \
--rawfile source revenue-lifecycle-v2.grid \
--arg baseSourceHash "$BASE_SOURCE_HASH" \
'{source: $source, baseSourceHash: $baseSourceHash}' \
> /tmp/revenue-source-v2.json
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X PUT "$GRID_URL/api/models/revenue-lifecycle/source" \
--data-binary @/tmp/revenue-source-v2.json \
| tee /tmp/revenue-lifecycle-updated.json \
| jq '{id, sourceHash, declaredInputs, declaredOutputs, diagnostics}'
Resolve TaxDue and NetRevenue. Depending on the deployed source-replacement state contract, confirm the current Revenue input from the response rather than assuming whether an authored default or resident input was retained. For Revenue = 120000, the outputs are TaxDue = 25200 and NetRevenue = 94800; for Revenue = 100000, they are 21000 and 79000.
See the exact read and replace source contract.
6. Cause a stale-source failure, then repair it
Prepare an attempted rollback to version 1 while deliberately retaining version 1's stale base hash. The server now owns version 2, so this is not an authorized compare-and-swap:
jq -n \
--rawfile source revenue-lifecycle.grid \
--arg baseSourceHash "$BASE_SOURCE_HASH" \
'{source: $source, baseSourceHash: $baseSourceHash}' \
> /tmp/revenue-source-stale.json
curl -sS -o /tmp/revenue-stale-body.json -w '%{http_code}\n' \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X PUT "$GRID_URL/api/models/revenue-lifecycle/source" \
--data-binary @/tmp/revenue-source-stale.json
jq . /tmp/revenue-stale-body.json
Expected failure: a non-2xx conflict response. Exact conflict codes can vary by route in this release, so retain both the HTTP status and complete body.
Minimal repair:
GET /api/models/revenue-lifecycle/sourceagain.- Reconcile your intended source with the returned source.
- Build a new request using the newly returned
sourceHash.
Do not repair a conflict by removing baseSourceHash; doing so silently changes compare-and-swap into last-writer-wins.
7. Lock the reviewed source with protected mode
Protected mode is an immutable source boundary in this release. Opt in only after the source update is complete. Redeploy the exact current version 2 source with the same model ID:
jq -n --rawfile source revenue-lifecycle-v2.grid '{
id: "revenue-lifecycle",
protectedMode: true,
source: $source
}' > /tmp/revenue-lifecycle-protected.json
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$GRID_URL/api/models" \
--data-binary @/tmp/revenue-lifecycle-protected.json \
| tee /tmp/revenue-lifecycle-protected-response.json \
| jq '{id, sourceHash, protectedMode, declaredInputs, declaredOutputs, diagnostics}'
Expected checkpoint: protectedMode = true and the source hash still identifies version 2. Live writes to declared inputs remain available, but source replacement is now locked.
Prove that boundary with a source request that uses the current hash, so the failure cannot be mistaken for stale compare-and-swap state:
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
"$GRID_URL/api/models/revenue-lifecycle/source" \
> /tmp/revenue-protected-source.json
jq -n \
--rawfile source revenue-lifecycle.grid \
--arg baseSourceHash "$(jq -r '.sourceHash' /tmp/revenue-protected-source.json)" \
'{source: $source, baseSourceHash: $baseSourceHash}' \
> /tmp/revenue-protected-edit.json
curl -sS -o /tmp/revenue-protected-edit-response.json -w '%{http_code}\n' \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X PUT "$GRID_URL/api/models/revenue-lifecycle/source" \
--data-binary @/tmp/revenue-protected-edit.json
jq . /tmp/revenue-protected-edit-response.json
Expected failure: a non-2xx response reporting GRID_PROTECTED_SOURCE_LOCKED. The model keeps version 2. Do not attempt to repair this by redeploying with protectedMode: false; protected mode cannot be downgraded in place. A genuine source revision needs an authorized replacement workflow, such as deleting and recreating this bounded tutorial model.
8. Rename, list, and delete
Rename only the display name:
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$GRID_URL/api/models/revenue-lifecycle/rename" \
--data '{"name":"FY27 Revenue Lifecycle"}' | jq
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
"$GRID_URL/api/model-summaries" \
| jq '.[] | select(.id == "revenue-lifecycle")'
Finally remove the temporary model:
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-X DELETE "$GRID_URL/api/models/revenue-lifecycle"
Expected response: the JSON boolean true. A later list should not contain the ID.
You are done when
- The locally generated deploy body contains the exact source asset.
- The checked response fixture passes, and the live create projection matches the exact source identity and declared contract.
- Readiness is
okbefore deployment and the request goes only to Node. - Baseline
NetRevenueis79,000, and the guarded input change produces94,800. - Source version 2 declares both
TaxDueandNetRevenue. - A stale source hash fails, and you can explain the refetch-and-reconcile repair.
- The reviewed version 2 source is locked by protected mode while declared input writes remain available.
- The renamed model is discoverable and deletion returns
true. - You can explain which requests are reads, which mutations use concurrency guards, and which ambiguous
POSToutcomes require inspection rather than blind retry.
For the complete route inventory, continue to Model lifecycle API. For retry and backpressure policy, use API conventions.