Build a loan approval pipeline that decides, orchestrates, escalates and explains itself - entirely from models, over the Priostack API. No engine to install and no application code to deploy.
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. Create a key from the dashboard, then 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
and evaluating a decision each cost one credit; deployments are free. A failed
evaluation refunds the credit automatically.
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.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.
Decision tables are evaluated directly, which means you can test policy before any
process exists. 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. Deployment is free; you are charged when applications run.
curl -s -X POST "$PRIOSTACK/api/v1/process-definitions" \
-H "X-API-Key: $PRIOSTACK_KEY" \
-F "file=@loan_approval.bpmn"
{
"id": "loan_approval_bpmn",
"key": 1787294406839,
"deployments": [
{
"processDefinitionKey": "25a18c795eb6893d3175",
"bpmnProcessId": "loan-approval",
"version": 1,
"resourceName": "loan_approval.bpmn"
}
]
}
Note version. Deploying the same bpmnProcessId again
creates version 2 and leaves running applications on version 1 - a policy change
never rewrites a decision already taken. 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
}
}'
{
"processInstanceKey": "1ce49bbc5260b39423bd",
"id": "1ce49bbc5260b39423bd",
"bpmnProcessId": "loan-approval",
"processDefinitionKey": "25a18c795eb6893d3175",
"version": 1
}
Keep processInstanceKey - it is how you follow this application for
the rest of its life. GET /api/v1/process-instances/{key} returns its
current state and variables, where state is
ACTIVE, COMPLETED or TERMINATED.
If you get a 402, the tenant is out of credits. Starting an application costs one credit; every account gets 100 free credits at signup and 100 every month.
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. Your worker polls for that job type:
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": "1ce49bbc5260b39423bd",
"processDefinitionKey": "25a18c795eb6893d3175",
"variables": { "applicant_id": "APP-4471" },
"retries": 3,
"deadline": 1787294689776
}
]
}
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. If the job is never completed
its deadline passes and it becomes activatable again, so a worker
that dies mid-task does not strand the application.
A process is the wrong model for an investigation. You cannot say what happens first, only what is available and what closes it. When the pipeline flags an unusual pattern, open a case instead:
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": "1ce49bbc5260b39423bd",
"reason": "velocity"
}
}'
The case carries the application key, so the investigator sees the pipeline that
raised it and the pipeline can wait on the case outcome. Human tasks inside the
case are completed through POST /api/v1/cases/jobs/{key}, and
GET /api/v1/cases lists what is open.
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.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 }
}'
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": "pst_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. Applications already running keep
the version they started under, and the next one picks up the new rates. 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 every endpoint used here, or the developer overview for the two ways to build on Priostack.