install.yaml Schema Reference
Canonical schema for the declarative install.yaml manifest consumed by plugin:install's manifest flow.
- Overview
- Top-Level Fields
- plugin_name
- pubspec / dev_pubspec / pubspec_assets (dependencies:)
- publish
- json_merge
- magic
- native
- env
- prompts
- placeholders
- post_install
- bootstrap_command
- Complete Example
Overview
install.yaml is the declarative manifest that drives plugin:install's manifest flow. The file is parsed by ManifestParser (lib/src/installer/manifest_parser.dart) into a typed InstallManifest (lib/src/installer/install_manifest.dart) and consumed by ManifestInstaller, which translates each section into deferred InstallOperations on the PluginInstaller transaction. Nothing writes to disk until commit(dryRun:, force:) fires.
The schema is intentionally tolerant: every section except plugin_name is optional, every section factory returns a sensible empty default when absent, and partial manifests parse cleanly. Schema violations raise ManifestValidationException; raw YAML syntax errors raise FormatException.
Top-Level Fields
| Field | Type | Required | Default | Source |
|---|---|---|---|---|
plugin_name |
string | yes | (none) | manifest_parser.dart:112 |
dependencies |
map | no | empty | manifest_parser.dart:124 |
publish |
map | no | empty | manifest_parser.dart:127 |
json_merge |
map | no | empty | manifest_parser.dart:128 |
magic |
map | no | empty | manifest_parser.dart:129 |
native |
map | no | empty | manifest_parser.dart:132 |
env |
map | no | empty | manifest_parser.dart:135 |
prompts |
list | no | empty | manifest_parser.dart:136 |
placeholders |
map | no | empty | manifest_parser.dart:137 |
post_install |
map | no | empty | manifest_parser.dart:138 |
bootstrap_command |
string | no | null |
manifest_parser.dart:141 |
plugin_name
The plugin's pubspec package name. Used to scope .artisan/installed/ reversibility records, the plugins.json registry entry, and the generated lib/app/_plugins.g.dart import line.
Validation regex (manifest_parser.dart:31):
^[a-z_][a-z0-9_]*$
The value must start with a lowercase letter or underscore, followed by any number of lowercase letters, digits, or underscores. An empty string raises ManifestValidationException('plugin_name must not be empty.').
plugin_name: example_plugin
dependencies
Pubspec mutations. The dependencies: map (note the YAML key is plural, but the typed model is PubspecDeps) carries three sub-keys, all optional.
| Sub-key | Type | Translates to |
|---|---|---|
pubspec |
Map |
dependencies: entries in the consumer's pubspec.yaml |
dev_pubspec |
Map |
dev_dependencies: entries |
pubspec_assets |
List |
Paths appended under flutter.assets: |
Empty maps and missing keys are interchangeable; both produce an empty PubspecDeps.
dependencies:
pubspec:
intl: ^0.20.0
dev_pubspec:
mocktail: ^1.0.0
pubspec_assets:
- assets/example/
publish
Stub-name to target-path mapping. Each entry becomes a publishConfig operation: the named stub is read from the plugin's install/ directory, placeholder-substituted, and written to the target path inside the consumer project.
| Key shape | Value shape |
|---|---|
| Stub name (string) | Target path (string) |
Non-string values raise FormatException('publish entry ".
publish:
install/example_config.dart.stub: lib/config/example.dart
json_merge
Map of target JSON file path to merge spec. Each entry triggers a mergeJson operation: the source stub is parsed as JSON, then merged into the target file according to additive.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
source |
string | yes | (none) | Stub key whose content is parsed as JSON. Missing or non-string raises FormatException('json_merge entry missing required "source" string.'). |
additive |
bool | no | true |
When true, existing keys in the target file survive a collision. When false, source keys overwrite target keys. |
json_merge:
assets/lang/en.json:
source: install/lang/en.json
additive: true
magic
Magic-framework integration block. Every slot is optional; the section defaults to an empty integration when absent.
| Field | Type | Translates to |
|---|---|---|
provider |
string (PascalCase) | injectProvider(provider) |
config_factory |
string | injectConfigFactory(name) |
routes |
string | injectRoute(name) |
Validation regex for provider (manifest_parser.dart:34):
^[A-Z][A-Za-z0-9]*$
The value, when present, must be a PascalCase identifier (uppercase first letter, alphanumerics afterwards). A non-matching value raises ManifestValidationException('magic.provider ".
magic:
provider: ExampleServiceProvider
config_factory: exampleConfig
routes: registerExampleRoutes
native
Per-platform native configuration. Each platform sub-section is independently optional.
native.android
| Field | Type | Translates to |
|---|---|---|
permissions |
List |
entries in AndroidManifest.xml |
meta_data |
Map |
entries in AndroidManifest.xml |
activities |
List<{name, exported?, task_affinity?, intent_filters?}> |
elements inside in AndroidManifest.xml |
gradle.plugins |
List<{id, version?}> |
Gradle plugin block entries |
gradle.dependencies |
List<{scope, notation}> |
Gradle dependency block entries |
Each activities entry needs name (an entry without it raises FormatException('Android activity missing required "name".')). exported is false when omitted. task_affinity sets android:taskAffinity; an empty string "" is an empty affinity and omitting the key omits the attribute. Each intent_filters entry takes auto_verify (bool), actions and categories (lists of strings) and data (a list of {scheme, host, path, path_prefix} maps, one element each).
An activity is idempotent by content. The same name with an equal set of intent filters (in any order) is left alone, and the same name with different filters is never rewritten: the install finishes and warns with the block it expected. exported and task_affinity are not part of that comparison.
Gradle plugin entries without id raise FormatException('Gradle plugin missing required "id".'). Gradle dependency entries without both scope and notation raise FormatException('Gradle dependency entry requires both "scope" and "notation".').
native:
android:
permissions:
- android.permission.INTERNET
meta_data:
io.flutter.embedded_views_preview: "true"
activities:
- name: com.example.CallbackActivity
exported: true
task_affinity: ""
intent_filters:
- auto_verify: true
actions: [android.intent.action.VIEW]
categories:
- android.intent.category.DEFAULT
- android.intent.category.BROWSABLE
data:
- {scheme: https, host: auth.example.com, path: /callback}
gradle:
plugins:
- id: com.example.gradle.plugin
version: "1.0.0"
dependencies:
- scope: implementation
notation: "com.example:lib:1.0.0"
native.ios
| Field | Type | Notes |
|---|---|---|
info_plist |
Map |
Values may be String, bool, num, or List. Runtime-type branching happens in the dispatcher. |
url_schemes |
List |
Custom URL schemes registered under CFBundleURLTypes, written without the :// suffix. One CFBundleURLTypes dict (role Editor) is appended per scheme; a scheme any existing dict already lists is skipped. |
entitlements |
Map |
Values may be String, bool, or a list of strings. Written to every entitlements file the application target signs with; see below. |
podfile.platform_version |
string (optional) | Informational in v1: no chain method consumes it yet. |
podfile.pods |
List |
Pod declarations appended to target 'Runner'. |
native:
ios:
info_plist:
NSExampleUsageDescription: "Reason shown in iOS permission dialog"
UIBackgroundModes: ["fetch"]
url_schemes:
- com.example.app
entitlements:
com.apple.security.keychain: true
com.apple.developer.applesignin: [Default]
podfile:
platform_version: "13.0"
pods:
- "ExamplePod"
entitlements keys go into every entitlements file the application target signs with, one per distinct CODE_SIGN_ENTITLEMENTS value across its build configurations, and the Xcode build settings are left as they are. A project that signs with none gets Runner/Runner.entitlements written and the target pointed at it. A list value is merged entry by entry into the array the file already carries, so entries another plugin or the app put there survive. A value that is not a string, a bool, or a list of strings fails the install. Both the plist and project.pbxproj writes are helper-backed and sit outside the transaction's .tmp rollback; see PluginInstaller DSL for the cases that warn instead of write.
native.macos
Shape mirrors native.ios exactly. Same field set (info_plist, url_schemes, entitlements, podfile.platform_version, podfile.pods), same dispatcher rules. A macOS Flutter project signs Debug and Release with different files (Runner/DebugProfile.entitlements, Runner/Release.entitlements), so an entitlement lands in both. See the Complete Example for a macOS block in context.
native.web
| Field | Type | Translates to |
|---|---|---|
head_scripts |
List |
HTML snippets injected before in web/index.html |
meta_tags |
List |
attribute maps appended to web/index.html |
native:
web:
head_scripts:
- ''
meta_tags:
- {name: "description", content: "Example plugin metadata"}
env
Environment variables to inject into the consumer's .env. Map of var name to spec.
| Field | Type | Required | Notes |
|---|---|---|---|
default |
scalar | yes | Default value written into .env. Coerced via toString(). Missing raises FormatException('env entry missing required "default".'). |
comment |
string | no | Advisory documentation. The v1 dispatcher does NOT emit the comment into the .env file. |
Non-map entries raise FormatException('env entry ".
env:
EXAMPLE_KEY:
default: example_value
comment: "Example plugin runtime config"
prompts
Ordered list of prompts driven immediately at the start of install (before any deferred operation runs). Prompt answers populate the prompt-result map that placeholders interpolates.
| Field | Type | Required | Notes |
|---|---|---|---|
key |
string | yes | Unique within the prompts list. Duplicates raise ManifestValidationException('Duplicate prompt key ". |
type |
string | yes | One of string, bool, choice. |
question |
string | yes | Human-readable question text. |
default |
scalar | no | Stored as a String regardless of type for uniform placeholder substitution. |
options |
List |
no | For choice prompts: the valid option set. Empty for non-choice types. |
Missing key, type, or question raises FormatException('Prompt entry requires "key", "type", and "question" strings.').
prompts:
- {key: configPath, type: string, default: "~/.example.conf", question: "Config file path?"}
- {key: mode, type: choice, options: [dev, staging, prod], default: dev, question: "Deployment mode?"}
- {key: enableFeature, type: bool, default: false, question: "Enable optional feature?"}
placeholders
Map of placeholder key to value template. Values may reference prompt answers via {{ prompts.KEY }}. Whitespace around the dotted reference is optional, so {{prompts.x}} and {{ prompts.x }} are both accepted.
Reference regex (manifest_parser.dart:39):
\{\{\s*prompts\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}
Every {{ prompts.X }} reference must resolve to a declared prompt key. Unknown references raise ManifestValidationException('Placeholder ".
placeholders:
configFilePath: "{{ prompts.configPath }}"
runtimeMode: "{{ prompts.mode }}"
post_install
Post-install shell operations plus a final info message. Every sub-field is optional.
| Field | Type | Notes |
|---|---|---|
run |
List<{cmd, args?}> |
Shell commands run unconditionally at commit phase. cmd is required; missing raises FormatException('Shell spec missing required "cmd".'). |
ask_to_run |
List<{prompt, cmd, args?}> |
Shell commands run only after user confirmation. The prompt fires immediately at install time; on yes the shell call is deferred. Missing prompt or cmd raises FormatException('ask_to_run entry requires "prompt" and "cmd" strings.'). |
message |
string | Optional info message emitted after a Success result. |
post_install:
run:
- cmd: dart
args: [format, lib/config/example.dart]
ask_to_run:
- prompt: "Run pub get now?"
cmd: flutter
args: [pub, get]
message: |
example_plugin installed.
bootstrap_command
Optional plugin-specific bootstrap command name AUTO-RUN after a Success result. On a non-dry-run install, once the plugin is registered (plugins.json + lib/app/_plugins.g.dart regenerated), plugin:install spawns the declared command as a fresh dispatcher subprocess so the just-registered plugin command actually executes. The chained command is a plugin command that was only just written to _plugins.g.dart, so it is not loaded in the current process; a subprocess is the only way to reach it.
Dispatcher resolution:
./bin/fsawhen a--non-interactive bin/fsawrapper exists at the consumer project root (the AOT fast-CLI path).- Otherwise
dart run, deriving the consumer package name from the project's:artisan --non-interactive pubspec.yamlname:field. - When neither resolves,
plugin:installprints the one-line bootstrap hint (Bootstrap with: artisan) instead, so the operator can run it manually.
--non-interactive is ALWAYS forwarded to the chained command so an interactive bootstrap (e.g. starter:install) cannot hang waiting on stdin.
Override and opt-out:
--bootstrap-command=on theplugin:installinvocation overrides the manifest value (the override command runs instead of the declared one).--no-bootstrapskips the auto-run entirely; the command falls back to printing the hint.
This was hint-only before 0.0.9: prior versions only printed Bootstrap with: artisan and never executed the command.
bootstrap_command: example:install
Complete Example
The example below is the canonical _fullYaml test fixture from test/installer/manifest_parser_test.dart. Every section above appears here in context.
plugin_name: example_plugin
dependencies:
pubspec:
intl: ^0.20.0
crypto: ^3.0.0
dev_pubspec:
mocktail: ^1.0.0
pubspec_assets:
- assets/example/
- assets/lang/en.json
publish:
install/example_config.dart.stub: lib/config/example.dart
install/example_routes.dart.stub: lib/routes/example.dart
json_merge:
assets/lang/en.json:
source: install/lang/en.json
additive: true
magic:
provider: ExampleServiceProvider
config_factory: exampleConfig
routes: registerExampleRoutes
native:
android:
permissions:
- android.permission.INTERNET
meta_data:
io.flutter.embedded_views_preview: "true"
activities:
- name: com.example.CallbackActivity
exported: true
intent_filters:
- auto_verify: true
actions: [android.intent.action.VIEW]
categories: [android.intent.category.DEFAULT, android.intent.category.BROWSABLE]
data:
- {scheme: https, host: auth.example.com, path: /callback}
gradle:
plugins:
- id: com.example.gradle.plugin
version: "1.0.0"
dependencies:
- scope: implementation
notation: "com.example:lib:1.0.0"
ios:
info_plist:
NSExampleUsageDescription: "Reason shown in iOS permission dialog"
UIBackgroundModes: ["fetch"]
url_schemes:
- com.example.app
entitlements:
com.apple.security.keychain: true
podfile:
platform_version: "13.0"
pods:
- "ExamplePod"
macos:
info_plist:
NSExampleUsageDescription: "Reason shown in macOS permission dialog"
entitlements:
com.apple.security.network.client: true
podfile:
platform_version: "11.0"
pods:
- "ExamplePod"
web:
head_scripts:
- ''
meta_tags:
- {name: "description", content: "Example plugin metadata"}
env:
EXAMPLE_KEY:
default: example_value
comment: "Example plugin runtime config"
prompts:
- {key: configPath, type: string, default: "~/.example.conf", question: "Config file path?"}
- {key: mode, type: choice, options: [dev, staging, prod], default: dev, question: "Deployment mode?"}
- {key: enableFeature, type: bool, default: false, question: "Enable optional feature?"}
placeholders:
configFilePath: "{{ prompts.configPath }}"
runtimeMode: "{{ prompts.mode }}"
post_install:
run:
- cmd: dart
args: [format, lib/config/example.dart]
ask_to_run:
- prompt: "Run pub get now?"
cmd: flutter
args: [pub, get]
message: |
example_plugin installed.
bootstrap_command: example:install