Manifest (olan-app.json)
{
"$schema": "https://olan.app/schemas/olan-app/v1.json",
"schemaVersion": 1,
"id": "com.example.overdue-tasks",
"name": "Geciken görevler",
"version": "0.1.0",
"sdk": "^1.8.0",
"minServerVersion": "0.1.0",
"description": "Projelerdeki geciken görevler",
"entry": "dist/app.js",
"icon": "assets/icon.png",
"surfaces": {
"app": { "title": "Geciken görevler" },
"project.widget": { "title": "Gecikenler" },
"settings": { "title": "Ayarlar" }
},
"permissions": ["projects:read"],
"settings": {
"threshold": { "type": "number", "label": "Gecikme eşiği (gün)", "default": 3 }
}
}
Unknown top-level fields are errors (manifest.unknown_field); only $schema is extra. The
allowed fields depend on schemaVersion (only 1 today).
server declares backend code that Olan runs (server/index.ts with @olan-app/sdk/backend;
backend.md). A server whose administrator turned backends off (OLAN_APPS_BACKEND=0) rejects it
with manifest.feature_not_available.
| Field | Required | Rule |
|---|---|---|
$schema |
no | "https://olan.app/schemas/olan-app/v1.json" (editor completion) |
schemaVersion |
yes | 1 |
id |
yes | Reverse domain, lowercase: [a-z0-9-]+(\.[a-z0-9-]+)+, ≤ 128. Never change it after publishing |
name |
yes | 1–80 characters |
version |
yes | Semver MAJOR.MINOR.PATCH (pre-release allowed). Raise it for every new release; an organization catalog (publish --org) never takes the same number twice. On the marketplace a checks_failed, rejected or withdrawn version number may be submitted again; approved or released ones may not (marketplace.md#publishing) |
sdk |
yes | @olan-app/sdk range: "*", "1.2.3", "^1.0.0", "~1.2.0" or ">=1.0.0 <2.0.0" (no ||, no 1.x). Use "^1.8.0" (the current SDK; olan-app init writes it; data needs ^1.1.0, useUser/olan.users ^1.2.0, the Phase 3 surfaces, cards, calendar/contacts/mail/meetings calls and remote ^1.3.0, backend code (server) ^1.4.0, remote.requests, olan.files, openUrl/openExternal, refetchIntervalMs, the components Tabs … ProgressBar and ai.tools timeoutMs ^1.5.0, project.tab, project.settings, olan.project.config and useProjectConfig ^1.6.0, task.action, issue.action, record.card, the olan.project links/statuses/labels/calendar/activity/approvals calls, projects:extend and projectTemplates ^1.7.0, olan.project.fields ^1.8.0) |
minServerVersion |
no | Oldest Olan server that may install it (semver). Use "0.1.0", the first Olan server with Olan Apps (olan-app init writes it); raise it only when the app needs a newer server. Olan's own version is still 0.x, so "1.0.0" would block every server today |
description |
no | ≤ 400 characters |
entry |
yes (apps) | Bundle path in the package: "dist/app.js" (relative .js/.mjs, no ..) |
icon |
no | Package path to a png/svg/jpg/webp, e.g. "assets/icon.png" (the file must exist) |
kind |
no | "app" (default) or "web-link" |
surfaces |
yes (apps) | At least one surface (below, surfaces.md) |
permissions |
no | Default []; ≤ 64; no duplicates (permissions.md) |
settings |
no | Settings fields (below) |
data |
no | App database collections (below, database.md). Apps only, not web links. Needs "sdk": "^1.1.0" and the db permission |
remote |
no | { "url", "jwksUrl", "requests"? } of the app's own server (server.md); implies the remote permission. "requests": true (default false, SDK 1.5) turns on olan.remote.request: Olan posts signed requests to {url}/requests on behalf of the person (remote-requests.md). profile:read needs remote (manifest.profile_requires_remote) |
events |
no | Event types sent to {remote.url}/events (server.md#events); needs remote and each event's permission |
ai |
no | { "tools": [ … ] }: assistant tools run by the app server (server.md#assistant-tools-aitools); needs remote, implies ai:tools. A tool's timeoutMs (1000–60000, default 15000; read-only tools at most 15000, SDK 1.5) sets how long Olan waits for it |
projectTemplates |
no | Project templates offered in "Yeni proje" while the app is installed (SDK 1.7, below) |
server |
no | Backend code Olan runs (backend.md): { "entry": "server/index.ts", "schedules"?: [{ name, cron, timeZone?, function? }] (≤ 10, ≥ 5 min apart), "events"?: { "<event type>": "<function>" }, "hooks"?: [{ name, function?, verify: { kind: "hmac-sha256"|"standard-webhooks"|"none", header?, secretSetting? } }] }; olan-app build bundles entry into dist/server.js. Handlers go in defineBackend's schedules/events/hooks maps by name; function (optional; required as the events value) only names a fallback in functions, and such a function is not callable from the UI (backend.md#handler-resolution) |
Data (olan.db) #
data declares the app's database collections (since SDK 1.1.0, still schemaVersion: 1):
"data": {
"version": 1,
"collections": {
"decisions": {
"scope": "organization",
"fields": {
"title": { "type": "string", "required": true, "maxLength": 200, "indexed": true },
"status": { "type": "string", "enum": ["open", "done"], "indexed": true },
"projectId": { "type": "ref:project", "indexed": true }
},
"rules": { "read": "any", "update": "any", "delete": "owner" }
}
}
}
Field types, options, limits, scopes, rules and schema changes: database.md. Errors:
manifest.data_* (errors.md).
Surfaces #
| Surface | Where it appears | Params | Root component |
|---|---|---|---|
app |
Full screen: Olan's app list (/apps/<installation>), sidebar, the permanent address /apps/a/<appId> (links.md) |
null, or { return } (links.md) |
Screen (required: ui.root_not_screen) |
project.widget |
A card on the summary page of the projects the app is enabled in | { projectId: string } |
Card (or EmptyState) |
project.tab |
A tab of the project page, in projects the app is enabled in (SDK 1.6) | { projectId: string } |
Screen (required) |
project.settings |
The app's settings of one project: project page → Uygulamalar → Ayarlar (SDK 1.6) | { projectId: string } |
Screen (required) |
settings |
Olan → Ayarlar → Uygulamalar, instead of the generated form | null |
Screen (required) |
chat.action, chat.command, chat.card, mail.sidebar, mail.action, calendar.event.action, meeting.panel |
Modals and panels in chat, email, calendar and meeting screens (surfaces.md) | the object it was opened on | Screen recommended (its title is the header; not checked) |
task.action, issue.action, record.card |
A task's or issue's "…" menu; the open button of the app's record card (SDK 1.7, projects the app is enabled in) |
{ projectId, task }, { projectId, issue }, { projectId, linkId, target, externalId, payload } |
Screen recommended |
app, project.widget, settings, project.tab and project.settings entries are
{ "title"?: string } (≤ 80; defaults to the app name); the other surfaces need "title", chat.command also "command" (surfaces.md). Every declared surface
must be defined in defineApp({ ... }) with the same name (app.surface_missing), and a surface
defined in code but not declared is never opened (app.surface_undeclared).
export default defineApp({ app: Overview, 'project.widget': OverdueCard, settings: SettingsScreen });
projectTemplates #
SDK 1.7: up to 5 project templates the organization's "Yeni proje" lists while the app is installed
for the person. A project made from one gets the template's work packages, statuses, labels, starter
tasks and task/issue templates, and the app is added to it; the statuses and labels are the app's
(members use them, the app manages them with olan.project.statuses / labels). A new app version
with the same key replaces the template.
"projectTemplates": [{
"key": "purchasing",
"name": "Satın alma",
"description": "Talep, onay, sipariş ve teslim",
"content": {
"version": 1,
"lists": [{ "key": "requests", "title": "Talepler", "steps": [{ "key": "approval", "title": "Onay" }] }],
"statuses": [
{ "key": "todo", "name": "Yeni talep", "color": "#8A94A6", "category": "unstarted" },
{ "key": "awaiting-approval", "name": "Onay bekliyor", "color": "#E0A526", "category": "started" },
{ "key": "done", "name": "Teslim alındı", "color": "#2F9E6E", "category": "completed" },
{ "key": "cancelled", "name": "İptal", "color": "#A1A7B3", "category": "cancelled" }
],
"labels": [{ "name": "Donanım", "color": "#5B8DEF", "groupName": "Tür" }]
}
}]
key^[a-z0-9][a-z0-9._-]{0,62}$, unique;name1–120;description≤ 2000;content≤ 64 KiB.content:version: 1;lists(1–50 work packages,key,title,code?,color?,steps?≤ 50);statuses?(≤ 30,{ key, name, color: #RRGGBB, category }; when given they replace the defaults, so include one perunstarted,started,completedandcancelled);labels?(≤ 100,{ name, color, groupName?, description? });tasks?(≤ 200) andrecordTemplates?(≤ 50) in the shape of Olan's own project templates;settings?(description,coverColor,estimateScale).- The manifest checks the shape (
manifest.project_template_invalid); the server checks references between keys and skips a template it cannot use.
Settings fields #
settings maps a key (^[A-Za-z][A-Za-z0-9_]{0,63}$, ≤ 64 fields) to a field:
| Property | Notes |
|---|---|
type |
"string", "number", "boolean" or "select" |
label |
Required, ≤ 80 |
description |
≤ 400 |
default |
Same type as the field; for select one of the option values |
required |
boolean |
secret |
boolean: stored encrypted, masked in admin screens. Apps without server still receive the value; with server the UI gets null and only backend code reads it (olan.secrets.get, backend.md#secrets) |
options |
select only: [{ "value": "TRY", "label": "Türk lirası" }], ≥ 1, unique values |
"settings": {
"currency": { "type": "select", "label": "Para birimi", "default": "TRY", "options": [
{ "value": "TRY", "label": "Türk lirası" }, { "value": "EUR", "label": "Euro" } ] },
"apiKey": { "type": "string", "label": "API anahtarı", "secret": true },
"showDone": { "type": "boolean", "label": "Bitenleri göster", "default": false }
}
Olan generates the settings form from these fields (unless the app has a settings surface).
Read values with useSettings() or olan.settings.get(); defaults apply to unset fields. In tests
and validate --strict the defaults apply too, overridden by fixtures/settings.json.
Web links #
{ "kind": "web-link", "url": "https://…" } describes a link instead of a packaged app: no
entry, no settings, no permissions, no remote/events/ai, only the app surface (optional), and icon is an
http(s) URL or an Olan icon name. Users add web links in Olan → Uygulamalar; they are not built
with this SDK.
Package (olan-app pack) #
dist/<id>-<version>.olanapp is a ZIP of olan-app.json, dist/app.js, dist/server.js (when
the manifest declares server), assets/**, a
digest.json (SHA-256 of every file and of the whole package) and, when signed (--dev-sign,
or olan-app.key.json exists), signature.json (Ed25519; optional keyId, cli.md#signing-and-publishing).
The package must stay ≤ 5 MiB. The bundles keep the license comments of bundled code at the end
of the file; other notices go in assets/NOTICE or assets/THIRD_PARTY_NOTICES (marketplace.md#automatic-checks). Organization admins upload it
(olan-app publish --org, or the admin panel's Uygulamalar section).