Calling the gateway
One HTTP endpoint runs a decision. Send a reference to a published schema and the inputs it declares, and you get the decision back.
POST https://app.ellaworks.ai/api/proxy/{orgSlug}/{environmentName}/decisionsorgSlug is your organization's slug and environmentName is the environment to run in, which decides the variables the instructions can use and the providers the run falls back across.
Authentication
Send an organization API key in either header:
X-API-Key: YOUR_API_KEYAuthorization: Bearer YOUR_API_KEY
Create keys under Settings, Account, API Keys. The key's owner needs access to the environment, as for reading it: a person who has only dev cannot run decisions in prod, and neither can their key. A key issued by agx login is scoped to other work and is refused here with 401.
The request
{
"schema": "support/duplicate-charge@1",
"inputs": { "ticket": "I was charged twice for my order." },
"metadata": { "orderId": "o-1" }
}| Field | |
|---|---|
schema | The reference of a published schema. See Versions and references. |
schemaDefinition | Instead of schema: a schema sent inline, checked like a published one and logged as Unpublished (ad hoc): with no name or version. Send exactly one of the two. Use it for tests and one-off questions, and a published schema for anything that has to be versioned. |
inputs | A value for each input the schema declares. Every required one must be there. |
provider | Optional. The name of one of your LLM providers. When set, the run goes to that provider only and never falls back. |
model | Optional. A model of that provider, with the same effect. |
metadata | Optional. Up to 20 entries, each a string (key up to 64 characters, value up to 512). It is kept in the decision log in every privacy mode and never sent to a provider, so keep personal data out of it. teamId and runId are the names the log's team and run columns read. |
timeoutMs | Optional. The time each provider gets, from 1 to 50000. A whole run is held to 50 seconds however many providers are tried. |
Leave out provider and model to let the environment choose and to get a fallback when a provider fails.
The answer
{
"id": "0b0c3f2e-1c4e-4f5b-9a37-2f6f8a1d9c11",
"decision": true,
"probabilities": { "true": 0.93, "false": 0.07 },
"confidence": 0.86,
"provider": "openai",
"model": "gpt-6-luna",
"schema": {
"ref": "support/duplicate-charge",
"version": "1.2.0",
"contentHash": "sha256:9d0e…"
},
"latencyMs": 412,
"attempts": [
{
"llmProviderId": "…",
"provider": "openai",
"model": "gpt-6-luna",
"status": "success",
"latencyMs": 398
}
],
"costUsd": 0.0001
}decisionistrueorfalsefor a yes/no, an option id (or a list of ids) for a pick, and a number for a score.probabilitieshas a key for each outcome:trueandfalse, each option id, or each level of the scale.confidenceruns from 0 (a coin flip) to 1. Act on a decision only when it is high enough for you; below that, hand the case to a person.idis the run's row in the decision log (Logs, Decisions).attemptslists every provider tried, in order, with how each went.latencyMsis the whole run, fallbacks included.costUsdis an estimate at list price and is left out for a model that is not priced.
curl
curl -X POST "https://app.ellaworks.ai/api/proxy/acme/prod/decisions" \
-H "X-API-Key: $ELLAWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"schema": "support/duplicate-charge@1",
"inputs": { "ticket": "I was charged twice for my order." }
}'TypeScript
interface DecisionAnswer {
id: string;
decision: boolean | string | string[] | number;
probabilities: Record<string, number>;
confidence: number;
provider: string;
model: string;
schema: { ref: string | null; version: string | null; contentHash: string };
latencyMs: number;
}
async function decide(
schema: string,
inputs: Record<string, string | number | boolean>,
): Promise<DecisionAnswer> {
const response = await fetch(
"https://app.ellaworks.ai/api/proxy/acme/prod/decisions",
{
method: "POST",
headers: {
"X-API-Key": process.env.ELLAWORKS_API_KEY ?? "",
"Content-Type": "application/json",
},
body: JSON.stringify({ schema, inputs }),
},
);
if (!response.ok) {
const failure = (await response.json()) as { error: string; code?: string };
throw new Error(`Decision failed (${response.status} ${failure.code ?? ""}): ${failure.error}`);
}
return (await response.json()) as DecisionAnswer;
}
const answer = await decide("support/duplicate-charge@1", {
ticket: "I was charged twice for my order.",
});
if (answer.decision === true && answer.confidence >= 0.8) {
// refund automatically
} else {
// send to a person
}Python
import os
import requests
URL = "https://app.ellaworks.ai/api/proxy/acme/prod/decisions"
def decide(schema: str, inputs: dict) -> dict:
response = requests.post(
URL,
headers={"X-API-Key": os.environ["ELLAWORKS_API_KEY"]},
json={"schema": schema, "inputs": inputs},
timeout=60,
)
if not response.ok:
failure = response.json()
raise RuntimeError(
f"Decision failed ({response.status_code} {failure.get('code', '')}): {failure['error']}"
)
return response.json()
answer = decide("support/duplicate-charge@1", {"ticket": "I was charged twice for my order."})
if answer["decision"] is True and answer["confidence"] >= 0.8:
... # refund automatically
else:
... # send to a personErrors
A failure is { "error": "…", "code": "…" }, with more fields where noted.
| Status | Meaning | What to do |
|---|---|---|
400 | The body is not valid, the reference is malformed (invalid_schema_ref), required inputs are missing (missing_inputs, with missingInputs), or a provider rejected the request itself (request_rejected, with attempts) | Fix the request. Retrying the same one fails the same way. |
401 | No key, a bad key, or a scoped key | Check the key. |
403 | You have no access to the environment, or to the private registry the schema is in (forbidden) | Ask an administrator for the grant. |
404 | The schema, the environment or the named provider does not exist | Check the names. |
413 | The body is over 1 MB | Send less. |
422 | No provider you have configured can run this schema (no_compatible_provider) | Add a provider, or change the schema. Retrying does not help. |
500 | A provider key could not be read, or a lookup failed on our side | Retry later, with a pause. |
502 | Every provider failed (all_providers_failed, with attempts) | Retry later, with a pause. The attempts say what each one answered. |
Good habits
- Set a timeout in your client a little above what you pass as
timeoutMs, or above 50 seconds if you pass none, and decide in advance what your code does when no decision arrives. - Check
confidencebefore acting, and keep a path for the cases below your bar. - Pin the version in production. See Versions and references.
- Do not put personal data in
metadata. What the log keeps ofinputsis an administrator's setting;metadatais always kept.