ποΈ Public Sector Example
From Algorithmated Permits to AI Agents
How to expose a public-sector permit process as a UAPF-powered AI capability.
Updated December 8, 2025 Β· ~10 min read
Short version: if you can algorithmate a public-sector permit process into BPMN + DMN + CMMN and package it as a .uapf, you can expose it as a governed AI βfront deskβ capability. This article shows how to do that using a generic building permit example.
1. Context β Public-sector permits and Algorithmation
Permits are a classic public-sector pain point:
- Long and opaque processing times.
- Many exceptions and manual reviews.
- High compliance and audit requirements.
In the Algorithmation approach, we do not leave this logic scattered across emails and ad-hoc Excel files. Instead, we describe it explicitly as:
- BPMN β main end-to-end flow (application received β checks β decision β notification).
- DMN β rule tables for zoning, environmental constraints, fees and risk flags.
- CMMN β case logic for manual review, appeals and escalation.
All of this is then packaged into a single UAPF archive, for example:
municipal-building-permit.uapf
ββ manifest.json
ββ models/
β ββ bpmn/
β β ββ building-permit-main.bpmn.xml
β ββ dmn/
β β ββ zoning-compliance.dmn.xml
β β ββ environmental-screening.dmn.xml
β β ββ fee-calculation.dmn.xml
β β ββ risk-flags.dmn.xml
β ββ cmmn/
β ββ manual-review.cmmn.xml
β ββ appeal-case.cmmn.xml
ββ schemas/
β ββ application-input.json
β ββ decision-output.json
ββ docs/
ββ README.md
The question we answer in this article is: how do we let AI agents safely use that algorithm, without ever bypassing the legal process?
2. The four layers β From UAPF to AI front desk
We use the same four-layer pattern as in the retail credit example, but applied to a government permit process.
- UAPF package β the formal algorithm: BPMN + DMN + CMMN + manifest + schemas.
- UAPF Engine β runtime executing the package (on-prem, controlled and auditable).
- MCP server β a thin adaptor exposing the engine as typed tools.
- AI agent β the assistant interface (citizen portal, internal clerk helper, call centre aide).
Citizen / Clerk
β
AI Agent (LLM + tools)
β
MCP client
β
UAPF MCP server (permits)
β
UAPF Engine HTTP API
β
municipal-building-permit.uapf
This respects traditional administrative boundaries: rules stay in the models; the AI agent becomes a guided, documented interface, not a replacement for the legal process.
3. Step 1 β Define a clear UAPF manifest for the permit service
We start from a manifest which describes the public-service capabilities we want to expose. A simplified example:
{
"uapfVersion": "0.1.0",
"id": "eu.algomation.examples.municipal-building-permit",
"name": "Municipal Building Permit",
"version": "1.0.0",
"models": {
"bpmn": [
"models/bpmn/building-permit-main.bpmn.xml"
],
"dmn": [
"models/dmn/zoning-compliance.dmn.xml",
"models/dmn/environmental-screening.dmn.xml",
"models/dmn/fee-calculation.dmn.xml",
"models/dmn/risk-flags.dmn.xml"
],
"cmmn": [
"models/cmmn/manual-review.cmmn.xml",
"models/cmmn/appeal-case.cmmn.xml"
]
},
"interfaces": {
"processes": [
{
"id": "BuildingPermitEndToEnd",
"name": "Building Permit Application",
"type": "bpmn:Process",
"inputSchema": "schemas/application-input.json",
"outputSchema": "schemas/decision-output.json"
}
],
"decisions": [
{
"id": "ZoningCompliance",
"name": "Zoning Compliance Check",
"type": "dmn:Decision",
"inputSchema": "schemas/application-input.json",
"outputSchema": "schemas/decision-output.json"
},
{
"id": "FeeCalculation",
"name": "Permit Fee Calculation",
"type": "dmn:Decision",
"inputSchema": "schemas/application-input.json",
"outputSchema": "schemas/decision-output.json"
}
],
"cases": [
{
"id": "ManualReviewCase",
"name": "Manual Planning Review",
"type": "cmmn:CaseFile",
"inputSchema": "schemas/application-input.json",
"outputSchema": "schemas/decision-output.json"
}
]
}
}
This is what the MCP server will use to present the service upwards to AI agents.
4. Step 2 β Provide a stable HTTP API for the permit engine
As with the credit example, we put a small HTTP facade in front of the engine. For a permit service, two endpoints are enough:
POST /uapf/permits/execute-processβ run the full BPMN process once.POST /uapf/permits/evaluate-decisionβ evaluate a DMN decision (e.g. zoning or fees).
Example Express router:
// src/uapf/permitsRouter.ts
import express from "express";
import { executePermitProcess, evaluatePermitDecision } from "./permitsRuntime";
export const permitsRouter = express.Router();
// Full end-to-end building permit process
permitsRouter.post("/execute-process", async (req, res, next) => {
try {
const { packageId, processId, input } = req.body;
const result = await executePermitProcess(packageId, processId, input);
res.json(result);
} catch (err) {
next(err);
}
});
// Single DMN decision (zoning/fees/risk)
permitsRouter.post("/evaluate-decision", async (req, res, next) => {
try {
const { packageId, decisionId, input } = req.body;
const result = await evaluatePermitDecision(packageId, decisionId, input);
res.json(result);
} catch (err) {
next(err);
}
});
We mount this under /uapf/permits alongside any other services:
// src/server.ts
import express from "express";
import { permitsRouter } from "./uapf/permitsRouter";
const app = express();
app.use(express.json());
app.use("/uapf/permits", permitsRouter);
app.listen(process.env.PORT ?? 3000);
The runtime implementation shows the structure of the data we return β suitable for audit and citizen-facing explanations:
// src/uapf/permitsRuntime.ts
type PermitDecisionStatus = "granted" | "refused" | "needs_manual_review";
type PermitDecision = {
applicationId: string;
status: PermitDecisionStatus;
zoningStatus: "compliant" | "non_compliant" | "requires_exception";
environmentalScreening: "not_required" | "screening_required" | "full_eia_required";
totalFee: number;
reasons: string[];
nextSteps: string[];
};
export async function executePermitProcess(
packageId: string,
processId: string,
input: any
): Promise<PermitDecision> {
// TODO: load municipal-building-permit.uapf and run BPMN + DMN + CMMN
// The structure is fixed; implementation details stay in the engine.
return {
applicationId: input.applicationId ?? "PERMIT-2025-0001",
status: "needs_manual_review",
zoningStatus: "requires_exception",
environmentalScreening: "screening_required",
totalFee: 375.0,
reasons: [
"Requested building height exceeds default zoning limit for the parcel.",
"Site is within a designated landscape protection zone."
],
nextSteps: [
"Planning officer review required.",
"Applicant may be asked for additional documentation.",
"Final decision deadline: 30 calendar days from application receipt."
]
};
}
export async function evaluatePermitDecision(
packageId: string,
decisionId: string,
input: any
) {
// TODO: call DMN engine for zoning or fee calculation
return {
decisionId,
outcome: "CONDITIONALLY_COMPLIANT",
reasons: [
"Zoning compliant subject to exception for height.",
"Environmental screening recommended due to protected landscape."
]
};
}
This keeps the public-service logic strongly typed and inspectable.
5. Step 3 β Wrap the permit engine as an MCP server
We now expose the permit service as an MCP server named uapf-permits. It provides tools that any MCP-enabled agent can call:
uapf_permit_describe_serviceβ high-level metadata from the manifest.uapf_permit_run_applicationβ run the full BPMN process for a single application.uapf_permit_evaluate_zoningβ evaluate a DMN zoning decision without running the full process.
// mcp/uapf-permits/src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import axios from "axios";
const server = new McpServer({
name: "uapf-permits",
version: "1.0.0"
});
const ENGINE_BASE =
process.env.UAPF_PERMITS_ENGINE_BASE ?? "http://localhost:3000/uapf/permits";
// Tool 1: describe the permit service
server.tool(
"uapf_permit_describe_service",
{
title: "Describe Municipal Building Permit Service",
description:
"Returns manifest-level metadata for the municipal building permit UAPF package.",
inputSchema: z.object({
packageId: z
.string()
.default("eu.algomation.examples.municipal-building-permit")
}),
outputSchema: z.object({
packageId: z.string(),
name: z.string(),
version: z.string(),
processes: z.array(z.any()),
decisions: z.array(z.any()),
cases: z.array(z.any())
})
},
async ({ input }) => {
// For this example we respond with static metadata mirroring the manifest
return {
packageId: input.packageId,
name: "Municipal Building Permit",
version: "1.0.0",
processes: [
{
id: "BuildingPermitEndToEnd",
bpmnProcessId: "building_permit_main",
entryPoint: "StartEvent_ApplicationReceived"
}
],
decisions: [
{ id: "ZoningCompliance", dmnDecisionId: "ZoningComplianceDecision" },
{ id: "FeeCalculation", dmnDecisionId: "FeeCalculationDecision" }
],
cases: [
{ id: "ManualReviewCase", cmmnCaseId: "ManualReview" },
{ id: "AppealCase", cmmnCaseId: "AppealProcedure" }
]
};
}
);
// Tool 2: run full permit application process
server.tool(
"uapf_permit_run_application",
{
title: "Run Building Permit Application Process",
description:
"Executes the algorithmated municipal building permit process for a single application.",
inputSchema: z.object({
packageId: z
.string()
.default("eu.algomation.examples.municipal-building-permit"),
processId: z.string().default("BuildingPermitEndToEnd"),
application: z.object({
applicationId: z.string(),
applicantName: z.string(),
applicantType: z.enum(["natural_person", "legal_entity"]),
parcelId: z.string(),
intendedUse: z.string(),
buildingHeightMeters: z.number(),
footprintAreaSqM: z.number(),
isHeritageSite: z.boolean(),
isProtectedLandscape: z.boolean()
})
}),
outputSchema: z.object({
applicationId: z.string(),
status: z.enum(["granted", "refused", "needs_manual_review"]),
zoningStatus: z.string(),
environmentalScreening: z.string(),
totalFee: z.number(),
reasons: z.array(z.string()),
nextSteps: z.array(z.string())
})
},
async ({ input }) => {
const response = await axios.post(`${ENGINE_BASE}/execute-process`, {
packageId: input.packageId,
processId: input.processId,
input: input.application
});
return response.data;
}
);
// Tool 3: evaluate zoning only
server.tool(
"uapf_permit_evaluate_zoning",
{
title: "Evaluate Zoning Compliance",
description:
"Evaluates the DMN zoning decision for a building permit application.",
inputSchema: z.object({
packageId: z
.string()
.default("eu.algomation.examples.municipal-building-permit"),
decisionId: z.string().default("ZoningCompliance"),
application: z.object({
parcelId: z.string(),
intendedUse: z.string(),
buildingHeightMeters: z.number(),
footprintAreaSqM: z.number(),
isHeritageSite: z.boolean(),
isProtectedLandscape: z.boolean()
})
}),
outputSchema: z.object({
decisionId: z.string(),
outcome: z.string(),
reasons: z.array(z.string())
})
},
async ({ input }) => {
const response = await axios.post(`${ENGINE_BASE}/evaluate-decision`, {
packageId: input.packageId,
decisionId: input.decisionId,
input: input.application
});
return response.data;
}
);
const transport = new StdioServerTransport();
transport.connect(server).catch((err) => {
console.error("Failed to start uapf-permits MCP server:", err);
process.exit(1);
});
The municipality keeps full control: the MCP server is just a small adaptor, not a new decision engine.
6. Step 4 β AI agent instructions for public permits
As with the credit case, the last step is to give the agent a clear operating manual. The key points are:
- The agent may explain and guide, but must not invent outcomes.
- All formal decisions come from the UAPF engine.
- Every answer should include a governance trace.
Example system prompt fragment:
You are the Municipal Permit Assistant.
You help citizens and clerks work with building permits by calling the
official algorithmated process implemented in the UAPF package
"eu.algomation.examples.municipal-building-permit" (version 1.0.0).
Tools available via Model Context Protocol:
- uapf_permit_describe_service
- uapf_permit_run_application
- uapf_permit_evaluate_zoning
When the user wants to check or file a building permit application:
1. Collect the necessary application data (parcel, intended use, height, area,
heritage/protection flags, applicant type).
2. Call uapf_permit_run_application once with the completed application.
3. Use the returned status, zoningStatus, environmentalScreening, totalFee,
reasons and nextSteps to answer.
4. Do NOT change the status or numeric values returned by the tool.
When the user only asks whether something is allowed on a parcel:
1. Use uapf_permit_evaluate_zoning with the available parameters.
2. Explain the outcome and reasons.
3. If required input is missing, ask the user for those fields before calling the tool.
Always include the following line in your answer:
"Result based on the algorithmated municipal building permit process
(UAPF package 'eu.algomation.examples.municipal-building-permit', v1.0.0)."
You may explain concepts (zoning, environmental screening, fees) in your own words,
but the final legal outcome is always the output returned by the UAPF tools.
This gives you a consistent, respectful way to modernise public services: the formal process is explicit and under institutional control; the AI agent simply makes it easier to navigate.
7. Next steps
- Add SLAs and deadlines β model statutory time limits as timers and milestones in BPMN/CMMN.
- Connect to registers β resolve parcel IDs and zoning layers via trusted registries in the UAPF runtime.
- Reuse the pattern β apply the same UAPF + MCP structure to business licences, signage permits and other administrative services.
Combined with the retail credit example, this shows that Algorithmation is not just for banks. It can be the backbone of transparent, explainable digital public services, with AI agents acting as a user-friendly, but strictly governed, front desk.
For a broader view on how UAPF scales from single workflows to an enterprise-wide algorithm layer, see From Single Processes to the Algorithmated Enterprise .