Tutorial · Advanced

Agentic Credit Pipeline

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.

⏱ 45 minutes 🔑 One API key 📦 BPMN · DMN · CMMN · ArchiMate

What this builds

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.

Why models rather than code. A rate change is an edit to a decision table, reviewable by the person accountable for it, deployed without a release. A branch encoded in application code is invisible to that person and needs an engineer, a pull request and a deploy to move.

What you need

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.

The shape of the app

Four models, each answering a different question:

What the app decides - DMN 1.3

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.

What the app does - BPMN 2.0

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.

What the app handles - CMMN 1.1

fraud_review.cmmn. A case, not a process, because an investigation has no fixed order. Tasks become available; a milestone closes it.

What the app is - ArchiMate

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.

1 · Decide - the policy tables

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"
  }
}
Outputs arrive as strings. Decision table results are text, so compare and cast deliberately - "7.4", not 7.4. This catches people out when a gateway condition silently fails to match a number.

2 · Orchestrate - deploy the process

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.

3 · Run an application

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.

4 · Do the work - service tasks

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.

5 · Escalate - open a case

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.

6 · Publish it as a service

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:

credit.quote

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.

credit.apply

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.

What comes back

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.

Mistakes are rejected, not absorbed. An unrecognised field, an unknown 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.

Extending it

Move policy without a deploy

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.

Let the platform explain the outcome

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.

Offer it to other apps

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.