APIZDocs

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 202 reply for /synthetic without 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

  1. Open Policies and select Create Policy.
  2. Choose Scripted.
  3. Select the General API adapter scope.
  4. Under Start from an example, choose Scripted request toolbox.
  5. 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:

  1. /synthetic calls api.reply with a bounded status, headers, and body;
  2. /trusted demonstrates an allow decision that can skip later optional Policies;
  3. /reserved attempts a forbidden query mutation and therefore fails closed;
  4. non-buffered bodies are denied;
  5. a valid buffered JSON body is rewritten through request.edit() and commit() before returning api.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, reason synthetic_response, status 202, 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 deny with stable error category and reason.

Select RunRun 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

  1. Select Review publication. APIZ saves and compiles as needed. Review the changes and traffic effect, then confirm the new Published Revision.
  2. Open Run tests from Policy detail.
  3. Select Run full suite and confirm Published suite passed.
  4. 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

  1. Open Clients and select a Client with a private General API Binding.
  2. Expand the Binding's Policies section.
  3. Choose the Scripted Policy and select Attach Policy.
  4. 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 / AuditAccess 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.