APIZ guide
Scripted Policy Tutorial
See Choose And Use An APIZ SDK for the host SDK versus Policy SDK boundary and runnable Python/TypeScript examples.
Build a General API Scripted Policy that demonstrates three bounded outcomes:
- return a synthetic
202reply for/syntheticwithout reaching upstream; - rewrite a buffered JSON request before it reaches upstream;
- fail closed when a script attempts a forbidden reserved-query mutation.
The tutorial uses the built-in Scripted request toolbox project. Complete either the Web Console or CLI path.
Understand The Runtime Boundary
Scripted Policy is authored as one TypeScript decide function. It receives a
sanitized Policy context, a bounded decision API, and a logical request API. It
cannot read upstream credentials, make network calls, access the filesystem, or
use arbitrary host globals. Compile errors, runtime errors, timeouts, invalid
patches, and oversized output deny the request.
Web Console
1. Create From The Scripted Example
- Open Policies and select Create Policy.
- Choose Scripted.
- Select the General API adapter scope.
- Under Start from an example, choose Scripted request toolbox.
- Review its three included Fixtures, then select Create from example.
The Policy starts inactive with an unpublished Working Draft. Selecting the example does not execute source or contact an upstream service.
2. Read The TypeScript Before Running It
Policy Studio opens policy.ts. Follow the branches in order:
/syntheticcallsapi.replywith a bounded status, headers, and body;/trusteddemonstrates an allow decision that can skip later optional Policies;/reservedattempts a forbidden query mutation and therefore fails closed;- non-buffered bodies are denied;
- a valid buffered JSON body is rewritten through
request.edit()andcommit()before returningapi.patch.
Use completion and hover to inspect the Policy SDK types. Local editor diagnostics are advisory; the server compile is authoritative. Format may format the current model but does not publish it.
3. Inspect And Run Fixtures
Open each Fixture in the Explorer and confirm its synthetic request and exact expectation:
- Return a synthetic response expects
reply, reasonsynthetic_response, status202, and a JSON body; - Patch the complete logical request expects the method, path, headers, query, and JSON body produced by the patch;
- Reject reserved query mutation expects a fail-closed
denywith stable error category and reason.
Select Run → Run all enabled Fixtures. The Studio saves and compiles the exact in-memory Draft when necessary, then opens the result panel. Inspect Summary, Assertions, Trace, Patch, and Sanitized output instead of relying only on the pass count.
4. Publish, Test Readiness, And Enable
- Select Review publication. APIZ saves and compiles as needed. Review the changes and traffic effect, then confirm the new Published Revision.
- Open Run tests from Policy detail.
- Select Run full suite and confirm Published suite passed.
- Select Enable Policy.
Draft test success is not a substitute for the published readiness run. The published suite is tied to the exact content hash that will be attached.
5. Attach To A Client Binding
- Open Clients and select a Client with a private General API Binding.
- Expand the Binding's Policies section.
- Choose the Scripted Policy and select Attach Policy.
- Keep it required at order
100.
Only move it to the API Instance layer if every Client using that connection should receive the same scripted behavior.
6. Verify Safe Results
Issue a short-lived Temporary Credential and call the APIZ Binding endpoint:
curl -i \
-H "Authorization: Bearer <apiz-temporary-credential>" \
"<apiz-general-endpoint>/synthetic"
Expected: APIZ returns 202 with x-policy-reply: yes and
{"accepted":true} without contacting upstream.
Then call /reserved. Expected: APIZ denies the invalid patch. Open Logs /
Audit → Access Logs and verify the stable script error evidence is
redacted and the upstream service was not reached.
The complete request-rewrite branch should be tried only against a test
upstream that safely accepts PATCH /v2/items.
CLI
1. Create The Policy Project
Create:
request-toolbox/
├── apiz-policy.json
├── policy.ts
└── fixtures/
├── patch-all.json
├── reply.json
└── reserved-query-deny.json
apiz-policy.json:
{
"api_version": "apiz.io/v1",
"kind": "PolicyProject",
"metadata": {
"name": "Scripted request toolbox",
"description": "Demonstrate bounded request edits, replies, and fail-closed decisions."
},
"config": {
"type": "scripted",
"adapter_scope": "general"
},
"code": {
"language": "typescript",
"entry": "policy.ts"
},
"fixtures": [
{ "file": "fixtures/patch-all.json" },
{ "file": "fixtures/reply.json" },
{ "file": "fixtures/reserved-query-deny.json" }
]
}
policy.ts:
import { definePolicy, type JSONValue } from "@apiz/policy-sdk";
function itemName(value: JSONValue): string | null {
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
return typeof value.name === "string" ? value.name : null;
}
export default definePolicy({
version: "v1",
decide(_ctx, api, request) {
if (request.path === "/synthetic") {
return api.reply("synthetic_response", {
status: 202,
headers: {"content-type": "application/json", "x-policy-reply": "yes"},
body: "{\"accepted\":true}",
});
}
if (request.path === "/reserved") {
const reserved = request.edit();
reserved.query.delete("secret");
return api.patch("must_not_apply", reserved.commit());
}
if (request.body.kind !== "buffered") return api.deny("body_not_buffered");
const name = itemName(request.body.json());
if (name === null) return api.deny("invalid_item");
const editor = request.edit();
editor.setMethod("PATCH");
editor.setPath("/v2/items");
editor.headers.set("X-Policy-Mode", "rewrite");
editor.query.set("version", "2");
editor.body.setJSON({accepted: true, name});
return api.patch("request_rewritten", editor.commit());
},
});
The tutorial source is intentionally shorter than the catalog example but uses the same bounded APIs and outcomes.
fixtures/reply.json:
{
"metadata": {
"key": "synthetic-reply",
"name": "Return a synthetic response",
"enabled": true,
"adapter_id": "general"
},
"request": { "method": "GET", "path": "/synthetic" },
"expectation": {
"decision": "reply",
"reason": "synthetic_response",
"reply": {
"status": 202,
"headers": {
"content-type": ["application/json"],
"x-policy-reply": ["yes"]
},
"body": { "encoding": "json", "value": { "accepted": true } }
}
}
}
fixtures/reserved-query-deny.json:
{
"metadata": {
"key": "deny-reserved-query",
"name": "Reject reserved query mutation",
"enabled": true,
"adapter_id": "general"
},
"request": { "method": "GET", "path": "/reserved" },
"expectation": {
"decision": "deny",
"reason": "policy_definition_error",
"error_category": "script_runtime_error",
"error_reason": "invalid_policy_patch_query"
}
}
fixtures/patch-all.json:
{
"metadata": {
"key": "patch-json",
"name": "Rewrite a JSON request",
"enabled": true,
"adapter_id": "general"
},
"request": {
"method": "POST",
"path": "/v1/items",
"content_type": "application/json",
"body": { "encoding": "json", "value": { "name": "demo" } }
},
"expectation": {
"decision": "patch",
"reason": "request_rewritten",
"request": {
"method": "PATCH",
"path": "/v2/items",
"content_type": "application/json",
"headers": { "X-Policy-Mode": ["rewrite"] },
"query": { "version": ["2"] },
"body": {
"encoding": "json",
"value": { "accepted": true, "name": "demo" }
}
}
}
}
2. Optional Project Test
Install the TypeScript builder from npm:
npm install --global @apiz/policy-sdk
Use the CLI quickstart to configure your deployment, sign in and select the intended Team before running:
apiz policy test --project ./request-toolbox --explain
The builder compiles your project locally; the CLI submits its fixtures to APIZ for evaluation. This requires a reachable Control Plane and permission to test Policies, but does not contact the upstream service. You can also run fixtures in Policy Studio. The server performs authoritative compilation during publish.
3. Publish, Run Readiness, And Enable
umask 077
apiz -o json policy publish --project ./request-toolbox \
> scripted-policy-review.json
POLICY_ID=$(jq -r '.candidate.policy_id' scripted-policy-review.json)
DRAFT_REVISION=$(jq -r '.candidate.draft_revision' scripted-policy-review.json)
POLICY_REVISION=$(jq -r '.candidate.current_published_revision' scripted-policy-review.json)
jq '{candidate, changes, evidence, traffic_effect}' scripted-policy-review.json
apiz -o json policy publish "$POLICY_ID" --project ./request-toolbox \
--expected-draft-revision "$DRAFT_REVISION" \
--expected-policy-revision "$POLICY_REVISION" \
--confirm > scripted-policy-publish.json
apiz policy test-suite "$POLICY_ID"
apiz policy enable "$POLICY_ID"
If server compilation rejects the TypeScript, no new Published Revision is promoted. Correct the source and publish again.
4. Attach And Verify
apiz client binding policy add <client-binding-id> \
--policy "$POLICY_ID" \
--order 100
Call /synthetic and /reserved using a Temporary Credential, then inspect:
apiz access-log list --client <client-id> --since 15m --limit 50
apiz access-log request <request-id>
Confirm that the reply and denial did not reach upstream and that the trace contains sanitized, stable Policy evidence rather than raw runtime stacks or credential values.
Production Checklist
- Keep every branch explicit; avoid implicit allow fallthrough.
- Add at least one Fixture for allow/reply/patch behavior and one for denial or failure behavior.
- Test streaming, malformed, and oversized inputs when the script reads a body.
- Publish, run the full published suite, then attach.
- Use a Client Binding attachment before broadening the rule to an API Instance.
- Investigate denied live requests through Access Logs; never add secrets to fixtures or logging to debug them.
Next: Understand request evidence.