Custom apps
How your organization will add its own record types, Actions, and views to Continual.
A custom app adds what your business needs and Continual's apps do not have: your own record types, Actions, and views. You define it the same way Continual's own apps are defined, and it runs beside them.
What a custom app is
Here is a small app for a trades business. It keeps jobs, shows them on a board and a calendar, and adds one Action that finishes a job.
import {
defineAction,
defineApp,
defineObject,
schema,
} from "@company/model/definition/index.ts"
import { Party } from "@company/model/platform/directory/parties.ts"
export const Job = defineObject({
type: "acme.trades.job",
collection: "acme.trades.jobs",
name: "Job",
pluralName: "Jobs",
description: "Work done at a customer's site, from booking to done.",
roles: { "platform.organizationMember": "all" },
properties: {
number: schema.counter({ label: "Number", prefix: "JOB" }),
title: schema.string({ label: "Title", minLength: 1, maxLength: 200 }),
customer: schema.reference.one(Party, {
label: "Customer",
inverse: { key: "jobs", label: "Jobs" },
}),
stage: schema.state(
[
{
value: "booked",
label: "Booked",
color: "gray",
category: "pending",
},
{
value: "onSite",
label: "On site",
color: "blue",
category: "active",
},
{ value: "done", label: "Done", color: "green", category: "succeeded" },
],
{ label: "Stage", default: "booked" }
),
visitDate: schema.date({ label: "Visit", nullable: true }),
quote: schema.money({ label: "Quote", nullable: true }),
},
indexes: {
stage: { fields: ["stage", "visitDate"] },
},
defaultViews: [
{ name: "Board", layout: "board", group: "stage" },
{ name: "Schedule", layout: "calendar", start: "visitDate" },
],
display: { title: ["title"], summary: ["customer", "visitDate", "quote"] },
})
export const FinishJob = defineAction({
key: "finish",
record: Job,
name: "Finish job",
description: "Marks the job done once the work is complete.",
transition: { from: ["booked", "onSite"], to: "done" },
})
export const TradesApp = defineApp({
id: "acme.trades",
publisher: "acme",
name: "Trades",
version: "1.0.0",
objects: [Job],
actions: [FinishJob],
})The parts:
defineObjectdeclares a record type: its fields, its stages, who can work on it, the views it starts with, and theindexesits views filter and sort by, here jobs by stage and visit date. A job's customer is any company or person in your records.defineActiondeclares an Action.FinishJobonly moves a job between stages, so it needs no code.defineApppackages them. Your app's names start with your organization's namespace, hereacme, so they never clash with Continual's.
Name records clearly
Every record shows its type, its number or handle, its title, and its status. Your definition declares each one, and Continual shows them the same way everywhere:
- Type:
nameandpluralName, such as "Job" and "Jobs". - Number: a
schema.counterfield, such asJOB-14. It is the record's handle, so it works wherever the record's ID does. Records that are named, not numbered, sethandleto a field such as a domain or a code instead. - Title:
display.title, the fields that say what the record is about. A title never names the counter, because the number already shows beside it and a draft has none. A title field may be a reference, such as a customer: the title then shows that record's own title. - Status: a
schema.statefield. Its stages' categories decide how lists and filters treat it.
For a record that is final once committed, such as an invoice or a work order, use
defineDocument instead of defineObject. A Document is a draft until its commit Action runs.
Committing numbers it and makes it final, and corrections are new Documents. See
Documents and numbers.
How it runs beside Continual's apps
From the definition alone, each record type gets what Continual's own record types get:
- Its standard operations, such as
acme.trades.jobs.list, in the REST API and as MCP tools. - Its pages in the web app, with the views you declared.
- Search, activity, and the record's history.
Your app's records follow the same rules as every other record. Members' roles decide who can read and change them. Policies can hold its Actions for a person's yes. Every change records who made it.
What works today
Installing your own app works through the API today. organizations.install takes the app's
manifest, the JSON form of its definition, and adds it to the organization the request works in.
Installing it again with changes updates it. Continual refuses a change that would drop a record
type that has records, or a field that holds values.
The SDK that writes manifests is not published yet. So this is not self-serve today.
Coming soon: ask Continual to build your app in your thread, and install it from the conversation.