Build a loan approval pipeline that decides, orchestrates, escalates and explains itself - from models, over the Priostack API. No engine to install; the only code you run is a worker for the steps only your systems can do.
A credit application arrives. A decision table prices the risk. A process routes it - auto-approve, refer to a human, or decline. If the pattern looks unusual, a case opens and an investigator picks up the thread. Every step is recorded, and the platform can explain in plain language why the application ended where it did.
What makes this worth building on Priostack is that none of it is code. The policy is a decision table you can hand to a risk officer. The routing is a process diagram. The investigation is a case model, because you cannot draw the order in which a fraud investigation unfolds. You submit those models and the platform runs them.
An API key, and nothing else. Everything below is HTTP. Your key arrives in the welcome email once you verify your address, and your account page shows it again whenever you need it. Keep it in your environment:
export PRIOSTACK_KEY="ps_your_key_here"
export PRIOSTACK="https://priostack.com"
Calls authenticate with the X-API-Key header. Starting an application,
evaluating a decision and opening a case cost one credit each. Deploying a model
through POST /api/v1/models (the DMN and CMMN files here)
costs one credit per model; deploying the BPMN through
POST /api/v1/process-definitions is free. Each of these calls refunds its
credit when it fails. Activating and completing jobs is free.
You also need a worker for step 4: a service task waits until something you run completes its job. Every other step is a single HTTP call.
Four models, each answering a different question:
credit_policy.dmn and risk_policy.dmn. Rate bands,
affordability thresholds, and the referral rule. Tabular, versioned, and
readable by the people who own the policy.
loan_approval.bpmn. The sequence: validate, score, decide, notify.
Service tasks hand work out to your own workers; gateways route on the decision
result rather than on hard-coded conditions.
fraud-review.cmmn. A case, not a process, because an investigation
has no fixed order. Tasks become available; a milestone closes it.
credit-app.archimate. The structure and the services it offers, which
is what lets the platform place this app inside a wider architecture rather than
treating it as an island.
/examples/: loan_approval.bpmn
(process id loan_approval_bpmn),
credit_policy.dmn (decision
credit_policy, inputs identity_verified and
risk_score), fraud_investigation.cmmn
(case fraud_investigation_case) and
ea_model.archimate. They deploy with the
same calls, under their own ids and inputs rather than the ones shown below.
Decision tables are evaluated directly, which means you can test policy before any
process exists. Deploy each table first. POST /api/v1/models detects
the kind of model from the XML and costs one credit, refunded if the deploy fails:
curl -s -X POST "$PRIOSTACK/api/v1/models?resourceName=credit_policy.dmn" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-H "Content-Type: application/xml" \
--data-binary @credit_policy.dmn
{
"key": "7c1e0b9d4a2f86e3b5d1",
"modelKind": "dmn",
"resourceName": "credit_policy.dmn",
"tenantId": "ps_your_key_here"
}
The answer is 201 Created. Deploy risk_policy.dmn the same
way. Until a decision is deployed, evaluating it answers 422
decision "credit_policy" not deployed for this tenant. Then evaluate
credit_policy with an applicant:
curl -s -X POST "$PRIOSTACK/api/v1/decisions/credit_policy/evaluate" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-H "Content-Type: application/json" \
-d '{
"annual_income": 48000,
"requested_amount": 12000,
"existing_debt": 3200,
"employment_months": 26
}'
The inputs are a flat JSON object whose keys match the table's input columns. The evaluation returns the outputs of the matched rule:
{
"result": {
"band": "B",
"rate": "7.4",
"max_advance": "15000",
"route": "refer"
}
}
"7.4", not 7.4. This
catches people out when a gateway condition silently fails to match a number.
Deploy the BPMN definition. Deploying BPMN through this route is free; each application you start costs one credit. Deploy the decision tables first (step 1): a business rule task whose decision is not deployed puts the application in INCIDENT.
curl -s -X POST "$PRIOSTACK/api/v1/process-definitions" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-F "file=@loan_approval.bpmn"
{
"id": "loan-approval",
"deployments": [
{
"processDefinitionKey": "25a18c795eb6893d3175",
"bpmnProcessId": "loan-approval",
"id": "loan-approval",
"version": 1,
"resourceName": "loan_approval.bpmn",
"tenantId": "ps_your_...here"
}
],
"key": 1787294406839,
"tenantId": "<default>"
}
The answer is 200 OK. The top-level id is the
bpmnProcessId of the first file deployed; the top-level key
is the server's clock in milliseconds, not an identifier.
Note version. Deploying a changed file with the same
bpmnProcessId creates version 2 and leaves running applications on
version 1; deploying the identical file again returns the existing version. New
applications always start on the newest version.
GET /api/v1/process-definitions lists what is deployed.
Start one instance per credit application, passing the applicant as variables:
curl -s -X POST "$PRIOSTACK/api/v1/process-instances" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-H "Content-Type: application/json" \
-d '{
"bpmnProcessId": "loan-approval",
"variables": {
"applicant_id": "APP-4471",
"annual_income": 48000,
"requested_amount": 12000,
"existing_debt": 3200,
"employment_months": 26
}
}'
{
"id": "run_1ce49bbc5260b39423bd",
"processInstanceKey": "run_1ce49bbc5260b39423bd",
"processDefinitionKey": "25a18c795eb6893d3175",
"bpmnProcessId": "loan-approval",
"version": 1,
"tenantId": "ps_your_...here"
}
The answer is 201 Created, with the header X-Credits-Spent: 1.
Keep processInstanceKey - it is how you follow this application for
the rest of its life. Keys are opaque strings: store them as they are and do not
parse them. GET /api/v1/process-instances/{key} returns its
current state and variables, where state is
ACTIVE, COMPLETED, INCIDENT or
TERMINATED.
If you get a 402, the tenant is out of credits. Starting an application costs one credit; every account gets 100 credits at signup and 100 more for each day since, on a rolling 24-hour basis. A credit pack from your wallet tops the balance up at once.
Where the process needs something only your systems can do - pull a bureau
report, write to the core banking ledger - it waits on a service task. This is
the step that needs your own worker: until something completes the job, the
application waits here. Your worker polls for that job type, and the answer comes
back at once ({"jobs":[]} when nothing waits, so sleep a few seconds
between empty polls):
curl -s -X POST "$PRIOSTACK/api/v1/jobs/activate" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "fetch-bureau-report", "worker": "bureau-worker", "maxJobsToActivate": 10 }'
{
"jobs": [
{
"key": "a99f579d62ae71f8764f",
"type": "fetch-bureau-report",
"processInstanceKey": "run_1ce49bbc5260b39423bd",
"processDefinitionKey": "25a18c795eb6893d3175",
"variables": { "applicant_id": "APP-4471" },
"retries": 3,
"deadline": 1787294689776,
"createdAt": "0001-01-01T00:00:00Z"
}
]
}
The type matches the task definition in your BPMN. Do the work, then
complete the job, merging what you learned back into the application:
curl -s -X POST "$PRIOSTACK/api/v1/jobs/a99f579d62ae71f8764f/complete" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-H "Content-Type: application/json" \
-d '{ "variables": { "bureau_score": 712, "bureau_flags": [] } }'
A successful completion returns 204 No Content - an empty body is
the success case here, not a silent failure.
The application resumes at the next element with bureau_score
available to every downstream gateway and decision.
deadline is informational (activation time plus five minutes, in Unix
milliseconds): nothing takes the job back when it passes. An activated job stays
with its worker until the worker completes it or fails it. A worker that has to
stop mid-task should release the job with POST /api/v1/jobs/{key}/fail
and {"retries": 3, "errorMessage": "shutting down"}, passing the job's
own retries so no attempt is used up; it is back in the queue at once.
Otherwise the application waits on that job until the server restarts.
A process is the wrong model for an investigation. You cannot say what happens first, only what is available and what closes it. Deploy the case model the same way as the decision tables (one credit):
curl -s -X POST "$PRIOSTACK/api/v1/models?resourceName=fraud-review.cmmn" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-H "Content-Type: application/xml" \
--data-binary @fraud-review.cmmn
When the pipeline flags an unusual pattern, open a case (one credit):
curl -s -X POST "$PRIOSTACK/api/v1/cases" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-H "Content-Type: application/json" \
-d '{
"caseDefinitionId": "fraud-review",
"variables": {
"applicant_id": "APP-4471",
"process_instance_key": "run_1ce49bbc5260b39423bd",
"reason": "velocity"
}
}'
The answer is 201 Created with the case's key, also a
run_ string. The case carries the application key as a variable, so
the investigator can find the pipeline that raised it; nothing else links the two,
and the process does not wait on the case. Human tasks inside the case are
completed through POST /api/v1/cases/jobs/{key}/complete, and
GET /api/v1/cases lists the tenant's cases.
So far this is your pipeline. Registering it on the platform is what lets other people's plans use it - the difference between an app that runs and an app that can be recruited.
A registration declares two different things. Scopes are what you want from the platform. Capabilities are what you can do for somebody else:
curl -s -X POST "$PRIOSTACK/api/v1/platform/register" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "credit",
"name": "Credit",
"description": "Price and decide consumer credit applications.",
"version": "1.0.0",
"category": "Finance",
"scopes": ["workflow:read_write", "decision:read_write", "credits:charge"],
"provides": [{
"id": "credit.quote",
"version": "1.0",
"title": "Price a credit application",
"inputs": ["annual_income", "requested_amount", "existing_debt"],
"outputs": ["band", "rate", "max_advance"],
"side_effect": "read_only",
"max_autonomy": 4,
"sensitivity": "normal",
"surface": "headless",
"timeout_ms": 2500
}, {
"id": "credit.apply",
"version": "1.0",
"title": "Submit a credit application",
"inputs": ["applicant_id", "requested_amount"],
"outputs": ["application_key", "route"],
"side_effect": "financial_commitment",
"requires_confirmation": true,
"max_autonomy": 2,
"sensitivity": "highly_sensitive",
"surface": "rendered",
"timeout_ms": 8000
}],
"emits": [{
"type": "credit.decided",
"fields": ["applicant_id", "route", "at"],
"sensitivity": "highly_sensitive"
}],
"bundle": { "models": [
{ "kind": "archimate", "path": "models/credit-app.archimate", "entry": true },
{ "kind": "bpmn", "path": "models/loan_approval.bpmn", "entry": true },
{ "kind": "dmn", "path": "models/credit_policy.dmn" },
{ "kind": "cmmn", "path": "models/fraud-review.cmmn" }
]},
"credit_costs": { "credit.apply": 1 }
}'
The bundle is a manifest: it records where your models live and deploys nothing. The models this app runs are the ones deployed in steps 1, 2 and 5.
Read the two capabilities against each other, because the difference is not a preference:
Pricing discloses nothing about who is borrowing and changes nothing, so it is
read_only at max_autonomy: 4 - a planner may call
it freely while costing out options.
Submitting commits money. financial_commitment caps it at
max_autonomy: 2, and the platform will refuse a higher ceiling
rather than take your word for it.
Registration returns your identity on the platform and, importantly, a receipt for what was accepted:
{
"app_id": "credit",
"scoped_token": "psvc-9f2c...",
"status": "registered",
"accepted": {
"capabilities": 2,
"events": 1,
"models": 4,
"scopes": 3,
"headless": true
}
}
Check the accepted block rather than the status code. It is there so
that a declaration which did not land is visible immediately, instead of showing
up weeks later as an app nobody's plan ever selects.
headless: true here is correct - this bundle ships no IFML views, so
the app has no interface of its own and whoever recruits it draws the step.
side_effect, a capability priced in
credit_costs that is not in provides, or a bundle with
two entry models of the same kind all return 400 naming the problem.
Use scoped_token for subsequent platform calls; the tenant API key
stays on your server.
Redeploy credit_policy.dmn alone. It replaces the table for your
tenant: every evaluation from then on uses the new rates, including applications
already running that have not reached the decision yet, while results already
written to an application do not change. No process redeploy and no code change.
Because the route came from a decision table and the path is recorded, the reason an application was referred is reconstructable rather than guessed - which is what makes an explanation you can put in front of a regulator different from one generated after the fact.
Once credit.quote is registered, any plan that needs credit pricing
can select it. That is the point of declaring capabilities rather than endpoints:
you describe an ability, and the resolver matches it to a need you never
anticipated.
Next: the API reference for the endpoints used here, or the developer overview for the two ways to build on Priostack.