Add an App
This guide walks through publishing a new actuarial model to the marketplace end-to-end — from an empty folder to a running app.
Prerequisites
- A public Git repository (GitHub, GitLab, or any URL the backend can
git clone) - Python knowledge to write the model logic
- The marketplace running locally (
docker compose up)
Step 1 — Create the repository
Create a new repository and initialise it. The backend will clone this URL on registration.
git init surrender-rate-model
cd surrender-rate-model
Your repo only needs two files at the root:
surrender-rate-model/
├── manifest.json
└── function.py
Everything else (README, tests, notebooks) is ignored by the marketplace.
Step 2 — Write manifest.json
Create manifest.json at the repo root. This file describes your app and defines the input form.
{
"name": "Surrender Rate Model",
"description": "Projects policyholder surrender rates under duration and rate stress scenarios.",
"inputs": {
"duration": {
"type": "number",
"label": "Policy duration",
"min": 1,
"max": 30,
"step": 1,
"default": 10,
"unit": " yrs"
},
"rate_stress": {
"type": "number",
"label": "Interest rate stress",
"min": -300,
"max": 300,
"step": 25,
"default": 0,
"unit": " bps"
}
}
}
Rules:
nameis the only required top-level field- Each key under
inputsbecomes a labelled slider in the form unitis displayed after the current slider value (e.g.10 yrs,+50 bps)
See the App Contract page for the full field reference.
Step 3 — Write function.py
Create function.py at the repo root. It must expose one async function named run:
import asyncio
import numpy as np
async def run(inputs: dict) -> dict:
await asyncio.sleep(3) # simulate computation
duration = int(inputs.get("duration", 10))
rate_stress_bps = float(inputs.get("rate_stress", 0))
years = list(range(1, duration + 1))
base_lapse = [0.15 * (0.82 ** y) + 0.02 for y in years]
rate_factor = 1 + max(0, rate_stress_bps) / 10000 * 5
lapse = [min(l * rate_factor, 0.99) for l in base_lapse]
in_force = []
current = 1.0
for l in lapse:
current *= (1 - l)
in_force.append(current)
return {
"columns": ["Year", "Lapse Rate", "In-Force"],
"series": [{"x": y, "y": round(s * 100, 2)} for y, s in zip(years, in_force)],
"table": [
{"year": y, "lapse_rate": f"{l*100:.2f}%", "in_force": f"{s*100:.1f}%"}
for y, l, s in zip(years, lapse, in_force)
],
"summary": {
"duration": f"{duration} years",
"rate_stress": f"{int(rate_stress_bps):+d} bps",
"terminal_in_force": f"{in_force[-1]*100:.1f}%",
},
}
Key rules for function.py:
- The function must be
async def run(inputs: dict) -> dict - Input keys match the keys defined in
manifest.json - The return value must be a JSON-serialisable dict
- Raise any exception to signal failure — the backend catches it and sets status to
FAILED
Step 4 — Push and register
Commit both files and push to your remote:
git add manifest.json function.py
git commit -m "initial model"
git remote add origin https://github.com/you/surrender-rate-model.git
git push -u origin main
Open the marketplace at http://localhost:3000 and click + Add App. Paste your repository URL and click Add.
What happens behind the scenes:
POST /apps/registerreceives{ repo_url: "https://…" }- The backend runs
git.Repo.clone_from(repo_url, dest) - Validates that both
manifest.jsonandfunction.pyexist - Parses the manifest and creates an
Approw in the database - The new card appears in the marketplace grid immediately
The repository URL must be publicly accessible — the backend clones over HTTPS without authentication. Private repos require adding an SSH key or access token to the backend container.
Step 5 — Run the app
Click Launch on your new card. The input form is generated automatically from your manifest.json. Adjust the sliders and click Run.
What happens:
POST /apps/{id}/runis called with your slider values- A
JobRunrow is created (status:PENDING) - A WebSocket opens and shows a live status indicator
- The backend dynamically loads your
function.pyand callsawait run(inputs) - On completion, the result (chart + table + summary) renders in the panel
Common errors
422 — manifest.json not found
The manifest must be at the root of the repository, not in a subdirectory.
✗ myrepo/models/manifest.json ← wrong
✓ myrepo/manifest.json ← correct
422 — function.py not found
Same rule: function.py must be at the repository root.
422 — Failed to clone repo
- Double-check the URL is correct and the repo is public
- GitHub: use
https://github.com/user/repo.git(with.git) - The backend needs network access to the external URL — check your Docker network settings
Status: FAILED after launching
The run function raised an unhandled exception. Check the backend logs:
docker compose logs backend -f
Common causes:
KeyError— an input key infunction.pydoesn't match themanifest.jsonkeyModuleNotFoundError— a library is imported that isn't installed in the backend imageTypeError— wrong type conversion (e.g.float(inputs["x"])whenxis already a number)
Duplicate app name
If a repo URL was already cloned, the backend deletes the old folder and re-clones. You can safely re-register an updated repo.
During development, push changes to your repo and re-register the URL in the marketplace. The old clone is replaced with the latest version.