# Custom apps

> How your organization will add its own record types, Actions, and views to Continual.

<Callout type="note" title="Coming soon">
  Custom apps are coming soon. Building and installing your own app is not
  self-serve yet. To build on Continual today, use the [REST
  API](/docs/developers/api) or [MCP](/docs/developers/mcp).
</Callout>

A custom app adds what your business needs and Continual's [apps](/docs/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.

```ts
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:

- **`defineObject`** declares a record type: its fields, its stages, who can work on it, the
  views it starts with, and the `indexes` its views filter and sort by, here jobs by stage and visit
  date. A job's customer is any company or person in your records.
- **`defineAction`** declares an Action. `FinishJob` only moves a job between stages, so it needs no
  code.
- **`defineApp`** packages them. Your app's names start with your organization's namespace, here
  `acme`, 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:** `name` and `pluralName`, such as "Job" and "Jobs".
- **Number:** a `schema.counter` field, such as `JOB-14`. It is the record's handle, so it works
  wherever the record's ID does. Records that are named, not numbered, set `handle` to 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.state` field. 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](/docs/concepts/records/documents).

## 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](/docs/developers/api)
  and as [MCP](/docs/developers/mcp) tools.
- Its pages in the web app, with the views you declared.
- Search, [activity](/docs/concepts/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](/docs/concepts/approvals) 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](/docs/concepts/threads), and install it
from the conversation.

<Cards>
  <Card title="REST API" href="/docs/developers/api" icon="code">
    Build on Continual today over HTTP.
  </Card>
  <Card
    title="Standard record types"
    href="/docs/developers/record-types"
    icon="list"
  >
    The record types Continual's apps define.
  </Card>
</Cards>
