Skip to content

Evaluate Policy from Source

Compile and evaluate a CNL (Controlled Natural Language) policy source string on the fly, without requiring the policy to be deployed first. Optionally returns a full decision trace for debugging.

Endpoint

POST /api/v1/policies/evaluate-source

Required Role

MEMBER

Query Parameters

ParameterTypeDefaultDescription
tracebooleanfalseWhen true, the response includes a structured decisionTrace object showing which rules fired and how the result was derived.

Headers

HeaderValueRequired
Content-Typeapplication/jsonYes
X-Tenant-IdTenant identifier stringYes
X-Aster-SignatureHMAC-SHA256 hex signatureYes
X-Aster-NonceRandom string (16+ bytes hex or UUID)Yes
X-Aster-TimestampUnix timestamp in millisecondsYes
X-User-RoleCaller's role (MEMBER, ADMIN, OWNER)Yes
X-User-IdCaller identifier for audit logsNo

See Authentication for full details on computing the HMAC signature.

Request Body

json
{
  "source": "string",
  "context": {},
  "locale": "en-US",
  "functionName": "evaluate"
}
FieldTypeRequiredDescription
sourcestringYesRaw CNL policy source text. The engine parses, canonicalizes, and compiles this string before evaluation. Parsing errors are returned in the error field.
contextobjectYesA single context object whose keys map to the declared parameter names of the target function. For multi-parameter functions, wrap each argument under its declared parameter name.
localestringNoBCP 47 locale tag that identifies the CNL keyword set used in source. Defaults to "en-US". Use "zh-CN" for Simplified Chinese CNL syntax.
functionNamestringNoName of the function to invoke within the compiled module. Defaults to "evaluate". If the source defines only one function, that function is called regardless of this value.

Response Body

json
{
  "result": "<any>",
  "executionTimeMs": 0,
  "error": null,
  "decisionTrace": null
}
FieldTypeDescription
resultanyThe value returned by the evaluated function.
executionTimeMsnumberTotal wall-clock time in milliseconds including compilation and execution.
errorstring | nullParse, compile, or runtime error message; null on success.
decisionTraceobject | nullPresent only when ?trace=true is set. Contains the module name, function name, and an ordered list of trace steps recording rule evaluation. null when tracing is disabled.

Decision Trace Structure

When ?trace=true is used, decisionTrace has the following shape:

json
{
  "moduleName": "string",
  "functionName": "string",
  "steps": [
    {
      "sequence": 1,
      "expression": "string",
      "result": "<any>",
      "matched": true,
      "children": []
    }
  ],
  "finalResult": "<any>",
  "executionTimeMs": 0
}
FieldTypeDescription
moduleNamestringThe name of the module that was evaluated.
functionNamestringThe name of the function that was invoked.
stepsTraceStep[]Ordered list of evaluation steps.
finalResultanyThe final result of the evaluation.
executionTimeMsnumberEvaluation time in milliseconds.

Each TraceStep contains:

FieldTypeDescription
sequencenumberStep number (starting from 1).
expressionstringThe rule or expression description.
resultanyThe evaluated result of this step.
matchedbooleanWhether this branch was the final match.
childrenTraceStep[]Nested sub-steps for branching logic (if/else, match).

HTTP Status Codes

StatusMeaning
200 OKCompilation and evaluation attempted. Check error for compile-time or runtime failures.
400 Bad RequestMalformed request body, missing required fields, or invalid X-Tenant-Id.
401 UnauthorizedMissing or invalid HMAC signature headers, or timestamp outside the 5-minute replay window.
403 ForbiddenHMAC is valid but the caller lacks the MEMBER role.
409 ConflictNonce has already been used within the replay window.
500 Internal Server ErrorUnexpected engine failure.

Examples

Without Trace

bash
TENANT_ID="acme-corp"
TIMESTAMP=$(($(date +%s) * 1000))
NONCE=$(openssl rand -hex 16)
BODY='{"source":"Module Discount.\n\nRule calculate given order as Order, produce Int:\n  If order.total greater than 100\n    Return 10.\n  Return 0.","context":{"order":{"total":150}},"locale":"en-US","functionName":"calculate"}'
API_SECRET="your-api-secret-here"
METHOD="POST"
PATH_URI="/api/v1/policies/evaluate-source"
QUERY=""

BODY_HASH=$(printf '%s' "${BODY}" | openssl dgst -sha256 | awk '{print $2}')
CANONICAL="${METHOD}|${PATH_URI}|${QUERY}|${TIMESTAMP}|${NONCE}|${BODY_HASH}"
SIGNATURE=$(printf '%s' "${CANONICAL}" | openssl dgst -sha256 -hmac "${API_SECRET}" | awk '{print $2}')

curl -X POST "https://policy.aster-lang.dev${PATH_URI}" \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: ${TENANT_ID}" \
  -H "X-User-Role: MEMBER" \
  -H "X-Aster-Signature: ${SIGNATURE}" \
  -H "X-Aster-Nonce: ${NONCE}" \
  -H "X-Aster-Timestamp: ${TIMESTAMP}" \
  -d "${BODY}"
js
const source = `Module Discount.

Define Order has total as Int.

Rule calculate given order as Order, produce Int:
  If order.total greater than 100
    Return 10.
  Return 0.`

const body = JSON.stringify({
  source,
  context: { order: { total: 150 } },
  locale: 'en-US',
  functionName: 'calculate',
})

const response = await fetch(
  'https://policy.aster-lang.dev/api/v1/policies/evaluate-source',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Tenant-Id': 'acme-corp',
      'X-User-Role': 'MEMBER',
      'X-Aster-Signature': signature,  // see Authentication docs
      'X-Aster-Nonce': nonce,
      'X-Aster-Timestamp': timestamp,
    },
    body,
  }
)

const data = await response.json()

With Decision Trace

bash
TENANT_ID="acme-corp"
TIMESTAMP=$(($(date +%s) * 1000))
NONCE=$(openssl rand -hex 16)
BODY='{"source":"Module Discount.\n\nDefine Order has total as Int.\n\nRule calculate given order as Order, produce Int:\n  If order.total greater than 100\n    Return 10.\n  Return 0.","context":{"order":{"total":150}},"locale":"en-US","functionName":"calculate"}'
API_SECRET="your-api-secret-here"
METHOD="POST"
PATH_URI="/api/v1/policies/evaluate-source"
QUERY="trace=true"

BODY_HASH=$(printf '%s' "${BODY}" | openssl dgst -sha256 | awk '{print $2}')
CANONICAL="${METHOD}|${PATH_URI}|${QUERY}|${TIMESTAMP}|${NONCE}|${BODY_HASH}"
SIGNATURE=$(printf '%s' "${CANONICAL}" | openssl dgst -sha256 -hmac "${API_SECRET}" | awk '{print $2}')

curl -X POST "https://policy.aster-lang.dev${PATH_URI}?${QUERY}" \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: ${TENANT_ID}" \
  -H "X-User-Role: MEMBER" \
  -H "X-Aster-Signature: ${SIGNATURE}" \
  -H "X-Aster-Nonce: ${NONCE}" \
  -H "X-Aster-Timestamp: ${TIMESTAMP}" \
  -d "${BODY}"
js
const source = `Module Discount.

Define Order has total as Int.

Rule calculate given order as Order, produce Int:
  If order.total greater than 100
    Return 10.
  Return 0.`

const body = JSON.stringify({
  source,
  context: { order: { total: 150 } },
  locale: 'en-US',
  functionName: 'calculate',
})

const response = await fetch(
  'https://policy.aster-lang.dev/api/v1/policies/evaluate-source?trace=true',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Tenant-Id': 'acme-corp',
      'X-User-Role': 'MEMBER',
      'X-Aster-Signature': signature,  // see Authentication docs
      'X-Aster-Nonce': nonce,
      'X-Aster-Timestamp': timestamp,
    },
    body,
  }
)

const data = await response.json()
// data.decisionTrace.steps → array of rule evaluation steps

Example Response (trace=true)

json
{
  "result": 10,
  "executionTimeMs": 12,
  "error": null,
  "decisionTrace": {
    "moduleName": "Discount",
    "functionName": "calculate",
    "steps": [
      {
        "sequence": 1,
        "expression": "order.total greater than 100",
        "result": 10,
        "matched": true,
        "children": []
      }
    ],
    "finalResult": 10,
    "executionTimeMs": 8
  }
}

Released under the MIT License.