Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 91 additions & 3 deletions docs/docs/development/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,14 @@ The plugin manifest drives the CLI's behavior during `databricks apps init`:
- **`app.yaml` generation** — resource fields produce `env` + `valueFrom` entries
- **`databricks.yml` generation** — resource fields produce bundle variables and app resource entries

The synced manifest is generated by `appkit plugin sync --write` from each plugin's `manifest.json` (see [Plugin manifest](../plugins/manifest.md) for the authoring contract). The on-disk shape carries a `version` field that the CLI uses to negotiate features:

- `"1.0"` / `"1.1"` — earlier shapes; still readable.
- `"2.0"` — current shape. Adds `scaffolding` (required by the CLI when `version` is `"2.0"`; enforced at parse time, not via the published JSON Schema) and the `origin` field on every resource field entry. JSON Schema published at `https://databricks.github.io/appkit/schemas/template-plugins.schema.json`.

### Resource field properties

Each resource field in the manifest can have these properties:
Each resource field in the synced manifest can have these properties:

| Property | Description |
|----------|-------------|
Expand All @@ -63,6 +68,21 @@ Each resource field in the manifest can have these properties:
| `value` | Default value used when no user input is provided |
| `resolve` | Auto-populated by CLI from API calls instead of prompting (see below) |
| `examples` | Example values shown in field descriptions |
| `discovery` | How the CLI lists candidate values for the field (see [Plugin manifest — Resource discovery](../plugins/manifest.md#resource-discovery)). |
| `origin` | **v2.0** computed field. How the value is determined — see below. |

### `origin` (v2.0)

`origin` is computed at sync time from the field's other properties — plugin authors do not write it. It tells scaffolding agents how each value reaches the running app:

| Origin | Trigger | Meaning |
|--------|---------|---------|
| `"platform"` | `localOnly: true` | Auto-injected by Databricks Apps at deploy time. Generated for local `.env` only; absent from `app.yaml` and bundle variables. |
| `"static"` | `value` set | Hardcoded literal. CLI does not prompt. |
| `"cli"` | `resolve` set | Resolved by the CLI from API calls (e.g. `postgres:host`). |
| `"user"` | none of the above | User must provide the value at init time. |

Precedence is in the order above (`localOnly` wins over `value`, which wins over `resolve`). The transform overwrites any hand-edited `origin` on the next sync — drift between the on-disk value and the field's actual shape is not possible by construction.

### Resolvers

Expand All @@ -89,7 +109,75 @@ Example field definition:
}
```

After sync, the field carries `"origin": "platform"` (because `localOnly` takes precedence over `resolve` for local-only fields injected at deploy time).

### `scaffolding.rules` propagation

Each plugin's `scaffolding.rules` block (see [Plugin manifest — Scaffolding rules](../plugins/manifest.md#scaffolding-rules)) is propagated unchanged into its entry in `appkit.plugins.json`. The CLI hands the merged plugin-level rules — alongside the top-level template `scaffolding.rules` — to scaffolding agents that drive `databricks apps init`.

Merge model:

1. Gather rules from every selected plugin and from every plugin with `requiredByTemplate: true`.
2. Apply the template-level `scaffolding.rules` on top.
3. Plugin-level rules **override** skill-baked or template-level defaults at the same directive site.
4. A plugin `must` that conflicts with a template `never` (or vice versa) stops the init flow — see the validation rules below.

### `scaffolding` descriptor (v2.0)

The `scaffolding` block at the top level of `appkit.plugins.json` describes the scaffolding command, its flags, and the cross-cutting rules every scaffolding agent must respect. It is required by the CLI when `version` is `"2.0"`. The requirement is enforced at parse time — the published JSON Schema marks only `version` and `plugins` as top-level required fields because it cannot express conditional requirements.

```json
{
"scaffolding": {
"command": "databricks apps init",
"flags": {
"--template-dir": {
"description": "Path to the template directory containing the app scaffold",
"required": true
},
"--config-dir": {
"description": "Path to the output directory for the initialized app",
"required": true
},
"--profile": {
"description": "Databricks CLI profile to use for authentication",
"required": false
}
},
"rules": {
"must": [
"Keep all secrets and credentials only in app.yaml, databricks.yml, and/or .env"
],
"should": [
"ask user when in doubt of resource to use for plugin"
],
"never": [
"guess resources when multiple or no options are available",
"embed secrets in files that will go to the client-bundle"
]
}
}
}
```

| Field | Description |
|-------|-------------|
| `command` | Scaffolding command the agent should invoke. |
| `flags` | Map of flag name to `{ description, required?, pattern?, default? }`. |
| `rules.must` | Actions the scaffolding agent must always perform. |
| `rules.should` | Recommended actions — applied unless overridden by a plugin-level rule. |
| `rules.never` | Actions the scaffolding agent must never perform. |

Each rule item is capped at **120 characters** by the schema. Long prose fails validation — split into discrete actionable directives.

The descriptor is canonical: AppKit owns the values and ships them with every synced template manifest. Authors of consuming agents (LLM-driven scaffolders, custom CLI runners) should treat the rule lists as enforcement contracts, not suggestions.

#### Substitutability gate (template rules)

The template-level rules survive the same substitutability gate that governs plugin-level rules: each entry must describe a **cross-cutting agent decision** the schema cannot already encode. Rules like "modify only files inside the template directory" or "list volumes after prompting for catalog/schema" are absent on purpose — the first is unreachable once you read the manifest as the source of truth, and the second is now encoded structurally as `parents: ["catalog", "schema"]` on the `volume` discovery kind (see [Plugin manifest — Transient prompts](../plugins/manifest.md#transient-prompts-parents)).

## See also

- [Plugin management](../plugins/plugin-management.md) — `appkit plugin sync`, `appkit plugin create`
- [Configuration](../configuration.mdx) — environment variables
- [Plugin manifest](../plugins/manifest.md) — the authoring side (`manifest.json`).
- [Plugin management](../plugins/plugin-management.md) — `appkit plugin sync`, `appkit plugin create`.
- [Configuration](../configuration.mdx) — environment variables.
55 changes: 32 additions & 23 deletions docs/docs/plugins/custom-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,41 @@ For a deeper understanding of the plugin structure, read on.

## Basic plugin example

Extend the [`Plugin`](../api/appkit/Class.Plugin.md) class and export with `toPlugin()`:
Author the manifest as JSON, import it, and attach it to a [`Plugin`](../api/appkit/Class.Plugin.md) subclass via `static manifest`. Export with `toPlugin()`:

```json
// my-plugin/manifest.json
{
"$schema": "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json",
"name": "my-plugin",
"displayName": "My Plugin",
"description": "A custom plugin",
"resources": {
"required": [
{
"type": "secret",
"alias": "apiKey",
"resourceKey": "api-key",
"description": "API key for external service",
"permission": "READ",
"fields": {
"scope": { "env": "MY_SECRET_SCOPE", "description": "Secret scope" },
"key": { "env": "MY_API_KEY", "description": "Secret key name" }
}
}
],
"optional": []
}
}
```

```typescript
// my-plugin/index.ts
import { Plugin, toPlugin, type PluginManifest } from "@databricks/appkit";
import type express from "express";
import manifest from "./manifest.json";

class MyPlugin extends Plugin {
static manifest = {
name: "myPlugin",
displayName: "My Plugin",
description: "A custom plugin",
resources: {
required: [
{
type: "secret",
alias: "apiKey",
resourceKey: "apiKey",
description: "API key for external service",
permission: "READ",
fields: {
scope: { env: "MY_SECRET_SCOPE", description: "Secret scope" },
key: { env: "MY_API_KEY", description: "Secret key name" }
}
}
],
optional: []
}
} satisfies PluginManifest<"myPlugin">;
static manifest = manifest as PluginManifest<"my-plugin">;

async setup() {
// Initialize your plugin
Expand All @@ -69,6 +76,8 @@ class MyPlugin extends Plugin {
export const myPlugin = toPlugin(MyPlugin);
```

JSON is the canonical authoring surface — it is what `appkit plugin sync` reads when aggregating manifests for templates. For the full v2.0 manifest contract (resources, discovery descriptors, scaffolding rules), see [Plugin manifest](./manifest.md).

## Config-dependent resources

The manifest defines resources as either `required` (always needed) or `optional` (may be needed).
Expand Down
Loading